Skip to content

Commit 1eab8e1

Browse files
authored
Deprecate Objective-C plugin template (#174003)
This PR marks the `--ios-language objc` flag as deprecated for plugin creation. It introduces a warning message in the `flutter create` tool when the flag is used, but **does not** remove the functionality at this time. This is the first step towards its eventual removal. ## Context As discussed in the issue, usage metrics show that Objective-C is used in less than 4% of newly created plugins. To streamline the tool and align with modern iOS development practices (i.e., Swift), we are beginning the process of phasing out this option **Changes:** - Adds a deprecation warning to the CLI when the `objc` flag is used. - Updates the `--help` text to reflect the deprecation. - Removes obsolete tests for Objective-C plugin creation and usage tracking. ## Testing I have validated these changes by: 1. Manually running `flutter-dev create` with the `objc` flag to confirm the new deprecation warning appears. 2. Verifying that creating a plugin without the --ios-language flag does **not** trigger the warning. 3. Running the tests in the `test/commands.shard` to ensure no regressions were introduced. 4. CI will validate the changes to the devicelab tests. Fixes #169683 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [] I updated/added relevant documentation (doc comments with `///`). - [] I added new tests to check the change I am making, or this PR is [test-exempt]. - [] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent cbac57a commit 1eab8e1

23 files changed

Lines changed: 571 additions & 943 deletions

File tree

dev/devicelab/bin/tasks/plugin_lint_mac.dart

Lines changed: 36 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -48,46 +48,9 @@ Future<void> main() async {
4848
await _tryMacOSLint(macosintegrationTestPodspec, <String>[]);
4949
});
5050

51-
section('Create Objective-C plugin');
51+
section('Create plugin');
5252

53-
const String objcPluginName = 'test_plugin_objc';
54-
await inDirectory(tempDir, () async {
55-
await flutter(
56-
'create',
57-
options: <String>[
58-
'--org',
59-
'io.flutter.devicelab',
60-
'--template=plugin',
61-
'--platforms=ios,android',
62-
'--ios-language=objc',
63-
objcPluginName,
64-
],
65-
);
66-
});
67-
68-
section('Lint Objective-C iOS podspec plugin as framework');
69-
70-
final String objcPluginPath = path.join(tempDir.path, objcPluginName);
71-
final String objcPodspecPath = path.join(objcPluginPath, 'ios', '$objcPluginName.podspec');
72-
await inDirectory(tempDir, () async {
73-
await exec('pod', <String>['lib', 'lint', objcPodspecPath, '--allow-warnings']);
74-
});
75-
76-
section('Lint Objective-C iOS podspec plugin as library');
77-
78-
await inDirectory(tempDir, () async {
79-
await exec('pod', <String>[
80-
'lib',
81-
'lint',
82-
objcPodspecPath,
83-
'--allow-warnings',
84-
'--use-libraries',
85-
]);
86-
});
87-
88-
section('Create Swift plugin');
89-
90-
const String swiftPluginName = 'test_plugin_swift';
53+
const String pluginName = 'test_plugin';
9154
await inDirectory(tempDir, () async {
9255
await flutter(
9356
'create',
@@ -96,44 +59,39 @@ Future<void> main() async {
9659
'io.flutter.devicelab',
9760
'--template=plugin',
9861
'--platforms=ios,macos',
99-
'--ios-language=swift',
100-
swiftPluginName,
62+
pluginName,
10163
],
10264
);
10365
});
10466

105-
section('Lint Swift iOS podspec plugin as framework');
67+
section('Lint iOS podspec plugin as framework');
10668

107-
final String swiftPluginPath = path.join(tempDir.path, swiftPluginName);
108-
final String swiftPodspecPath = path.join(swiftPluginPath, 'ios', '$swiftPluginName.podspec');
69+
final String pluginPath = path.join(tempDir.path, pluginName);
70+
final String podspecPath = path.join(pluginPath, 'ios', '$pluginName.podspec');
10971
await inDirectory(tempDir, () async {
110-
await exec('pod', <String>['lib', 'lint', swiftPodspecPath, '--allow-warnings']);
72+
await exec('pod', <String>['lib', 'lint', podspecPath, '--allow-warnings']);
11173
});
11274

113-
section('Lint Swift iOS podspec plugin as library');
75+
section('Lint iOS podspec plugin as library');
11476

11577
await inDirectory(tempDir, () async {
11678
await exec('pod', <String>[
11779
'lib',
11880
'lint',
119-
swiftPodspecPath,
81+
podspecPath,
12082
'--allow-warnings',
12183
'--use-libraries',
12284
]);
12385
});
12486

125-
section('Lint Swift macOS podspec plugin as framework');
87+
section('Lint macOS podspec plugin as framework');
12688

127-
final String macOSPodspecPath = path.join(
128-
swiftPluginPath,
129-
'macos',
130-
'$swiftPluginName.podspec',
131-
);
89+
final String macOSPodspecPath = path.join(pluginPath, 'macos', '$pluginName.podspec');
13290
await inDirectory(tempDir, () async {
13391
await _tryMacOSLint(macOSPodspecPath, <String>['--allow-warnings']);
13492
});
13593

136-
section('Lint Swift macOS podspec plugin as library');
94+
section('Lint macOS podspec plugin as library');
13795

13896
await inDirectory(tempDir, () async {
13997
await _tryMacOSLint(macOSPodspecPath, <String>['--allow-warnings', '--use-libraries']);
@@ -149,7 +107,7 @@ Future<void> main() async {
149107
);
150108
});
151109

152-
section('Build iOS application with Swift and Objective-C plugins as frameworks');
110+
section('Build iOS application with plugins as frameworks');
153111

154112
final String appPath = path.join(tempDir.path, iosAppName);
155113

@@ -160,7 +118,7 @@ Future<void> main() async {
160118
// Add the new plugins we just made.
161119
pubspecContent = pubspecContent.replaceFirst(
162120
'\ndependencies:\n',
163-
'\ndependencies:\n $objcPluginName:\n path: $objcPluginPath\n $swiftPluginName:\n path: $swiftPluginPath\n url_launcher: 6.0.16\n url_launcher_macos:\n',
121+
'\ndependencies:\n $pluginName:\n path: $pluginPath\n url_launcher: 6.0.16\n url_launcher_macos:\n',
164122
);
165123
pubspec.writeAsStringSync(pubspecContent, flush: true);
166124

@@ -174,7 +132,7 @@ Future<void> main() async {
174132
return TaskResult.failure('Expected default Podfile to contain use_frameworks');
175133
}
176134

177-
section('Build iOS application with Swift and Objective-C plugins as libraries');
135+
section('Build iOS application with plugins as libraries');
178136

179137
iosPodfileContent = iosPodfileContent.replaceAll('use_frameworks!', '');
180138
iosPodfile.writeAsStringSync(iosPodfileContent, flush: true);
@@ -198,26 +156,15 @@ Future<void> main() async {
198156

199157
_validateMacOSPodfile(appPath);
200158

201-
section('Build macOS application with plugins as libraries');
202-
203-
macosPodfileContent = macosPodfileContent.replaceAll('use_frameworks!', '');
204-
macOSPodfile.writeAsStringSync(macosPodfileContent, flush: true);
205-
206-
await inDirectory(appPath, () async {
207-
await flutter('build', options: <String>['macos']);
208-
});
209-
210-
_validateMacOSPodfile(appPath);
211-
212159
section('Remove iOS support from plugin');
213160

214-
Directory(path.join(objcPluginPath, 'ios')).deleteSync(recursive: true);
161+
Directory(path.join(pluginPath, 'ios')).deleteSync(recursive: true);
215162

216163
const String iosPlatformMap = '''
217164
ios:
218-
pluginClass: TestPluginObjcPlugin''';
165+
pluginClass: TestPlugin''';
219166

220-
final File pluginPubspec = File(path.join(objcPluginPath, 'pubspec.yaml'));
167+
final File pluginPubspec = File(path.join(pluginPath, 'pubspec.yaml'));
221168
String pluginPubspecContent = pluginPubspec.readAsStringSync();
222169
if (!pluginPubspecContent.contains(iosPlatformMap)) {
223170
return TaskResult.failure('Plugin pubspec.yaml missing iOS platform map');
@@ -237,10 +184,9 @@ Future<void> main() async {
237184
final String podfileLockOutput = podfileLockFile.readAsStringSync();
238185
if (!podfileLockOutput.contains(':path: ".symlinks/plugins/url_launcher_ios/ios"') ||
239186
!podfileLockOutput.contains(':path: Flutter')
240-
// test_plugin_objc no longer supports iOS, shouldn't be present.
187+
// test_plugin no longer supports iOS, shouldn't be present.
241188
||
242-
podfileLockOutput.contains(':path: ".symlinks/plugins/test_plugin_objc/ios"') ||
243-
!podfileLockOutput.contains(':path: ".symlinks/plugins/test_plugin_swift/ios"')) {
189+
podfileLockOutput.contains(':path: ".symlinks/plugins/test_plugin/ios"')) {
244190
print(podfileLockOutput);
245191
return TaskResult.failure('Podfile.lock does not contain expected pods');
246192
}
@@ -249,10 +195,19 @@ Future<void> main() async {
249195

250196
checkDirectoryExists(path.join(pluginSymlinks, 'url_launcher_ios', 'ios'));
251197

252-
checkDirectoryExists(path.join(pluginSymlinks, 'test_plugin_swift', 'ios'));
198+
// test_plugin no longer supports iOS, shouldn't exist!
199+
checkDirectoryNotExists(path.join(pluginSymlinks, 'test_plugin'));
253200

254-
// test_plugin_objc no longer supports iOS, shouldn't exist!
255-
checkDirectoryNotExists(path.join(pluginSymlinks, 'test_plugin_objc'));
201+
section('Build macOS application with plugins as libraries');
202+
203+
macosPodfileContent = macosPodfileContent.replaceAll('use_frameworks!', '');
204+
macOSPodfile.writeAsStringSync(macosPodfileContent, flush: true);
205+
206+
await inDirectory(appPath, () async {
207+
await flutter('build', options: <String>['macos']);
208+
});
209+
210+
_validateMacOSPodfile(appPath);
256211

257212
return TaskResult.success(null);
258213
} catch (e, stackTrace) {
@@ -271,8 +226,7 @@ void _validateIosPodfile(String appPath) {
271226
final String podfileLockOutput = podfileLockFile.readAsStringSync();
272227
if (!podfileLockOutput.contains(':path: ".symlinks/plugins/url_launcher_ios/ios"') ||
273228
!podfileLockOutput.contains(':path: Flutter') ||
274-
!podfileLockOutput.contains(':path: ".symlinks/plugins/test_plugin_objc/ios"') ||
275-
!podfileLockOutput.contains(':path: ".symlinks/plugins/test_plugin_swift/ios"') ||
229+
!podfileLockOutput.contains(':path: ".symlinks/plugins/test_plugin/ios"') ||
276230
podfileLockOutput.contains('url_launcher_macos')) {
277231
print(podfileLockOutput);
278232
throw TaskResult.failure('iOS Podfile.lock does not contain expected pods');
@@ -288,9 +242,7 @@ void _validateIosPodfile(String appPath) {
288242

289243
checkDirectoryNotExists(path.join(pluginSymlinks, 'url_launcher_macos'));
290244

291-
checkDirectoryExists(path.join(pluginSymlinks, 'test_plugin_objc', 'ios'));
292-
293-
checkDirectoryExists(path.join(pluginSymlinks, 'test_plugin_swift', 'ios'));
245+
checkDirectoryExists(path.join(pluginSymlinks, 'test_plugin', 'ios'));
294246

295247
// Make sure no Xcode build settings are leaking derived data/build directory into the ios directory.
296248
checkDirectoryNotExists(path.join(appPath, 'ios', 'build'));
@@ -305,9 +257,7 @@ void _validateMacOSPodfile(String appPath) {
305257
!podfileLockOutput.contains(
306258
':path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos',
307259
) ||
308-
!podfileLockOutput.contains(
309-
':path: Flutter/ephemeral/.symlinks/plugins/test_plugin_swift/macos',
310-
) ||
260+
!podfileLockOutput.contains(':path: Flutter/ephemeral/.symlinks/plugins/test_plugin/macos') ||
311261
podfileLockOutput.contains('url_launcher_ios/')) {
312262
print(podfileLockOutput);
313263
throw TaskResult.failure('macOS Podfile.lock does not contain expected pods');
@@ -328,7 +278,7 @@ void _validateMacOSPodfile(String appPath) {
328278

329279
checkDirectoryNotExists(path.join(pluginSymlinks, 'url_launcher_ios'));
330280

331-
checkDirectoryExists(path.join(pluginSymlinks, 'test_plugin_swift', 'macos'));
281+
checkDirectoryExists(path.join(pluginSymlinks, 'test_plugin', 'macos'));
332282
}
333283

334284
Future<void> _tryMacOSLint(String podspecPath, List<String> extraArguments) async {

dev/devicelab/bin/tasks/plugin_test_ios.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,9 @@ import 'package:flutter_devicelab/tasks/plugin_tests.dart';
88
Future<void> main() async {
99
await task(
1010
combine(<TaskFunction>[
11-
PluginTest('ios', <String>['-i', 'objc', '--platforms=ios']).call,
12-
PluginTest('ios', <String>['-i', 'swift', '--platforms=ios']).call,
11+
PluginTest('ios', <String>['--platforms=ios']).call,
1312
// Test that app builds with Flutter as a transitive dependency.
1413
PluginTest('ios', <String>[
15-
'-i',
16-
'objc',
1714
'--platforms=ios',
1815
], cocoapodsTransitiveFlutterDependency: true).call,
1916
// Test that Dart-only plugins are supported.

dev/devicelab/lib/tasks/plugin_tests.dart

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,15 +420,6 @@ public class $pluginClass: NSObject, FlutterPlugin {
420420
Map<String, String>? environment,
421421
}) async {
422422
final bool isDarwin = target == 'ios' || target == 'macos';
423-
if (template != 'plugin' && isDarwin) {
424-
// ios-language option is only supported for plugins. Remove the -i flag and the next parameter, "swift" or
425-
// "objc". This isn't proper arg parsing (for example doesn't handle -i=objc, but good enough for these tests
426-
// since they blow up if -i is passed incorrectly.
427-
final int indexOfIOSLanguage = options.indexOf('-i');
428-
if (indexOfIOSLanguage != -1) {
429-
options.removeRange(indexOfIOSLanguage, indexOfIOSLanguage + 2);
430-
}
431-
}
432423
await inDirectory(directory, () async {
433424
await flutter(
434425
'create',

packages/flutter_tools/lib/src/commands/create.dart

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ class CreateCommand extends FlutterCommand with CreateBase {
7070
defaultsTo: 'swift',
7171
allowed: <String>['objc', 'swift'],
7272
help:
73-
'(deprecated) The language to use for iOS-specific code, either Swift (recommended) or Objective-C (legacy). '
74-
'Only supported for "--template=plugin".',
73+
'(deprecated) This option is deprecated and no longer has any effect. '
74+
'Swift is always used for iOS-specific code. '
75+
'This flag will be removed in a future version of Flutter.',
7576
hide: !verboseHelp,
7677
);
7778
argParser.addOption(
@@ -163,7 +164,6 @@ class CreateCommand extends FlutterCommand with CreateBase {
163164
commandHasTerminal: hasTerminal,
164165
createProjectType: stringArg('template'),
165166
createAndroidLanguage: stringArg('android-language'),
166-
createIosLanguage: stringArg('ios-language'),
167167
);
168168

169169
// Lazy-initialize the net utilities with values from the context.
@@ -334,21 +334,11 @@ class CreateCommand extends FlutterCommand with CreateBase {
334334
exitCode: 2,
335335
);
336336
} else if (argResults!.wasParsed('ios-language')) {
337-
if (generateMethodChannelsPlugin) {
338-
globals.printWarning(
339-
'The "ios-language" option is deprecated and will be removed in a future Flutter release.',
340-
);
341-
if (stringArg('ios-language') == 'objc') {
342-
globals.printWarning(
343-
'Please comment in https://github.com/flutter/flutter/issues/169683 describing your use-case for using Objective-C instead of Swift.',
344-
);
345-
}
346-
} else {
347-
throwToolExit(
348-
'The "ios-language" option is only supported for "--template=plugin".',
349-
exitCode: 2,
350-
);
351-
}
337+
globals.printWarning(
338+
'The "--ios-language" option is deprecated and no longer has any effect. '
339+
'Swift is always used for iOS-specific code. '
340+
'This flag will be removed in a future version of Flutter.',
341+
);
352342
}
353343

354344
final String organization = await getOrganization();
@@ -425,7 +415,6 @@ class CreateCommand extends FlutterCommand with CreateBase {
425415
withFfiPackage: generateFfiPackage,
426416
withEmptyMain: emptyArgument,
427417
androidLanguage: stringArg('android-language'),
428-
iosLanguage: stringArg('ios-language'),
429418
iosDevelopmentTeam: developmentTeam,
430419
ios: includeIos,
431420
android: includeAndroid,

packages/flutter_tools/lib/src/commands/create_base.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,6 @@ mixin CreateBase on FlutterCommand {
296296
String? projectDescription,
297297
String? androidLanguage,
298298
String? iosDevelopmentTeam,
299-
String? iosLanguage,
300299
required String flutterRoot,
301300
required String dartSdkVersionBounds,
302301
String? agpVersion,
@@ -357,7 +356,6 @@ mixin CreateBase on FlutterCommand {
357356
'withPluginHook': withFfiPluginHook || withFfiPackage || withPlatformChannelPluginHook,
358357
'withEmptyMain': withEmptyMain,
359358
'androidLanguage': androidLanguage,
360-
'iosLanguage': iosLanguage,
361359
'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty,
362360
'iosDevelopmentTeam': iosDevelopmentTeam ?? '',
363361
'flutterRevision': escapeYamlString(globals.flutterVersion.frameworkRevision),

packages/flutter_tools/lib/src/reporting/custom_dimensions.dart

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ class CustomDimensions {
2727
this.commandRunProjectType,
2828
this.commandRunProjectHostLanguage,
2929
this.commandCreateAndroidLanguage,
30-
this.commandCreateIosLanguage,
3130
this.commandRunProjectModule,
3231
this.commandCreateProjectType,
3332
this.commandPackagesNumberPlugins,
@@ -83,7 +82,6 @@ class CustomDimensions {
8382
final String? commandRunProjectType; // cd14
8483
final String? commandRunProjectHostLanguage; // cd15
8584
final String? commandCreateAndroidLanguage; // cd16
86-
final String? commandCreateIosLanguage; // cd17
8785
final bool? commandRunProjectModule; // cd18
8886
final String? commandCreateProjectType; // cd19
8987
final int? commandPackagesNumberPlugins; // cd20
@@ -161,8 +159,6 @@ class CustomDimensions {
161159
if (commandCreateAndroidLanguage != null)
162160
CustomDimensionsEnum.commandCreateAndroidLanguage.cdKey: commandCreateAndroidLanguage
163161
.toString(),
164-
if (commandCreateIosLanguage != null)
165-
CustomDimensionsEnum.commandCreateIosLanguage.cdKey: commandCreateIosLanguage.toString(),
166162
if (commandRunProjectModule != null)
167163
CustomDimensionsEnum.commandRunProjectModule.cdKey: commandRunProjectModule.toString(),
168164
if (commandCreateProjectType != null)
@@ -276,7 +272,6 @@ class CustomDimensions {
276272
other.commandRunProjectHostLanguage ?? commandRunProjectHostLanguage,
277273
commandCreateAndroidLanguage:
278274
other.commandCreateAndroidLanguage ?? commandCreateAndroidLanguage,
279-
commandCreateIosLanguage: other.commandCreateIosLanguage ?? commandCreateIosLanguage,
280275
commandRunProjectModule: other.commandRunProjectModule ?? commandRunProjectModule,
281276
commandCreateProjectType: other.commandCreateProjectType ?? commandCreateProjectType,
282277
commandPackagesNumberPlugins:
@@ -356,7 +351,6 @@ class CustomDimensions {
356351
map,
357352
CustomDimensionsEnum.commandCreateAndroidLanguage,
358353
),
359-
commandCreateIosLanguage: _extractString(map, CustomDimensionsEnum.commandCreateIosLanguage),
360354
commandRunProjectModule: _extractBool(map, CustomDimensionsEnum.commandRunProjectModule),
361355
commandCreateProjectType: _extractString(map, CustomDimensionsEnum.commandCreateProjectType),
362356
commandPackagesNumberPlugins: _extractInt(
@@ -475,7 +469,8 @@ enum CustomDimensionsEnum {
475469
commandRunProjectType, // cd14
476470
commandRunProjectHostLanguage, // cd15
477471
commandCreateAndroidLanguage, // cd16
478-
commandCreateIosLanguage, // cd17
472+
// ignore: unused_field
473+
_nullCreateIosLanguageDeprecatedDoNotUse, // cd17
479474
commandRunProjectModule, // cd18
480475
commandCreateProjectType, // cd19
481476
commandPackagesNumberPlugins, // cd20

0 commit comments

Comments
 (0)