Skip to content

Commit 194fefa

Browse files
authored
[iOS] Migrate @UIApplicationMain attribute to @main (#146707)
This migrates Flutter to use the `@main` attribute introduced in Swift 5.3. The `@UIApplicationMain` attribute is deprecated and will be removed in Swift 6. See: https://github.com/apple/swift-evolution/blob/main/proposals/0383-deprecate-uiapplicationmain-and-nsapplicationmain.md This change is split into two commits: 1. flutter/flutter@ad18797 - This updates the iOS app template and adds a migration to replace `@UIApplicationMain` uses with `@main`. 2. flutter/flutter@8ecbb2f - I ran `flutter run` on each Flutter iOS app in this repository to verify the app migrates and launches successfully. Part of flutter/flutter#143044
1 parent 5a0369d commit 194fefa

File tree

14 files changed

+173
-20
lines changed

14 files changed

+173
-20
lines changed

dev/a11y_assessments/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import Flutter
66
import UIKit
77

8-
@UIApplicationMain
8+
@main
99
@objc class AppDelegate: FlutterAppDelegate {
1010
override func application(
1111
_ application: UIApplication,

dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import Flutter
66
import UIKit
77

8-
@UIApplicationMain
8+
@main
99
@objc class AppDelegate: FlutterAppDelegate {
1010
override func application(
1111
_ application: UIApplication,

dev/integration_tests/ios_app_with_extensions/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import Flutter
66
import UIKit
77

8-
@UIApplicationMain
8+
@main
99
@objc class AppDelegate: FlutterAppDelegate {
1010
override func application(
1111
_ application: UIApplication,

dev/integration_tests/ios_host_app_swift/Host/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import UIKit
66

7-
@UIApplicationMain
7+
@main
88
class AppDelegate: UIResponder, UIApplicationDelegate {
99
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
1010
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)

dev/integration_tests/non_nullable/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import Flutter
66
import UIKit
77

8-
@UIApplicationMain
8+
@main
99
@objc class AppDelegate: FlutterAppDelegate {
1010
override func application(
1111
_ application: UIApplication,

dev/manual_tests/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import Flutter
66
import UIKit
77

8-
@UIApplicationMain
8+
@main
99
@objc class AppDelegate: FlutterAppDelegate {
1010
override func application(
1111
_ application: UIApplication,

examples/api/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import UIKit
66
import Flutter
77

8-
@UIApplicationMain
8+
@main
99
@objc class AppDelegate: FlutterAppDelegate {
1010
override func application(
1111
_ application: UIApplication,

examples/platform_channel_swift/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ enum MyFlutterErrorCode {
1919
static let unavailable = "UNAVAILABLE"
2020
}
2121

22-
@UIApplicationMain
22+
@main
2323
@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {
2424
private var eventSink: FlutterEventSink?
2525

packages/flutter_tools/lib/src/ios/mac.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import 'migrations/project_base_configuration_migration.dart';
3535
import 'migrations/project_build_location_migration.dart';
3636
import 'migrations/remove_bitcode_migration.dart';
3737
import 'migrations/remove_framework_link_and_embedding_migration.dart';
38+
import 'migrations/uiapplicationmain_deprecation_migration.dart';
3839
import 'migrations/xcode_build_system_migration.dart';
3940
import 'xcode_build_settings.dart';
4041
import 'xcodeproj.dart';
@@ -158,6 +159,7 @@ Future<XcodeBuildResult> buildXcodeProject({
158159
XcodeScriptBuildPhaseMigration(app.project, globals.logger),
159160
RemoveBitcodeMigration(app.project, globals.logger),
160161
XcodeThinBinaryBuildPhaseInputPathsMigration(app.project, globals.logger),
162+
UIApplicationMainDeprecationMigration(app.project, globals.logger),
161163
];
162164

163165
final ProjectMigration migration = ProjectMigration(migrators);

packages/flutter_tools/lib/src/ios/migrations/remove_bitcode_migration.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@ class RemoveBitcodeMigration extends ProjectMigrator {
1616
final File _xcodeProjectInfoFile;
1717

1818
@override
19-
Future<bool> migrate() async {
19+
Future<void> migrate() async {
2020
if (_xcodeProjectInfoFile.existsSync()) {
2121
processFileLines(_xcodeProjectInfoFile);
2222
} else {
2323
logger.printTrace('Xcode project not found, skipping removing bitcode migration.');
2424
}
25-
26-
return true;
2725
}
2826

2927
@override
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import '../../base/file_system.dart';
6+
import '../../base/project_migrator.dart';
7+
import '../../xcode_project.dart';
8+
9+
const String _appDelegateFileBefore = r'''
10+
@UIApplicationMain
11+
@objc class AppDelegate''';
12+
13+
const String _appDelegateFileAfter = r'''
14+
@main
15+
@objc class AppDelegate''';
16+
17+
/// Replace the deprecated `@UIApplicationMain` attribute with `@main`.
18+
///
19+
/// See:
20+
/// https://github.com/apple/swift-evolution/blob/main/proposals/0383-deprecate-uiapplicationmain-and-nsapplicationmain.md
21+
class UIApplicationMainDeprecationMigration extends ProjectMigrator {
22+
UIApplicationMainDeprecationMigration(
23+
IosProject project,
24+
super.logger,
25+
) : _appDelegateSwift = project.appDelegateSwift;
26+
27+
final File _appDelegateSwift;
28+
29+
@override
30+
Future<void> migrate() async {
31+
// Skip this migration if the project uses Objective-C.
32+
if (!_appDelegateSwift.existsSync()) {
33+
logger.printTrace(
34+
'ios/Runner/AppDelegate.swift not found, skipping @main migration.',
35+
);
36+
return;
37+
}
38+
39+
// Migrate the ios/Runner/AppDelegate.swift file.
40+
final String original = _appDelegateSwift.readAsStringSync();
41+
final String migrated = original.replaceFirst(_appDelegateFileBefore, _appDelegateFileAfter);
42+
if (original == migrated) {
43+
return;
44+
}
45+
46+
logger.printWarning(
47+
'ios/Runner/AppDelegate.swift uses the deprecated @UIApplicationMain attribute, updating.',
48+
);
49+
_appDelegateSwift.writeAsStringSync(migrated);
50+
}
51+
}

packages/flutter_tools/lib/src/xcode_project.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,15 +178,15 @@ class IosProject extends XcodeBasedProject {
178178

179179
File get appFrameworkInfoPlist => _flutterLibRoot.childDirectory('Flutter').childFile('AppFrameworkInfo.plist');
180180

181+
/// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C.
182+
File get appDelegateSwift => _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift');
183+
181184
File get infoPlist => _editableDirectory.childDirectory('Runner').childFile('Info.plist');
182185

183186
Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');
184187

185-
/// True, if the app project is using swift.
186-
bool get isSwift {
187-
final File appDelegateSwift = _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift');
188-
return appDelegateSwift.existsSync();
189-
}
188+
/// True if the app project uses Swift.
189+
bool get isSwift => appDelegateSwift.existsSync();
190190

191191
/// Do all plugins support arm64 simulators to run natively on an ARM Mac?
192192
Future<bool> pluginsSupportArmSimulator() async {

packages/flutter_tools/templates/app_shared/ios-swift.tmpl/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Flutter
22
import UIKit
33

4-
@UIApplicationMain
4+
@main
55
@objc class AppDelegate: FlutterAppDelegate {
66
override func application(
77
_ application: UIApplication,

packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/ios/migrations/project_base_configuration_migr
1313
import 'package:flutter_tools/src/ios/migrations/project_build_location_migration.dart';
1414
import 'package:flutter_tools/src/ios/migrations/remove_bitcode_migration.dart';
1515
import 'package:flutter_tools/src/ios/migrations/remove_framework_link_and_embedding_migration.dart';
16+
import 'package:flutter_tools/src/ios/migrations/uiapplicationmain_deprecation_migration.dart';
1617
import 'package:flutter_tools/src/ios/migrations/xcode_build_system_migration.dart';
1718
import 'package:flutter_tools/src/ios/xcodeproj.dart';
1819
import 'package:flutter_tools/src/migrations/cocoapods_script_symlink.dart';
@@ -906,7 +907,7 @@ platform :ios, '12.0'
906907
project,
907908
testLogger,
908909
);
909-
expect(await migration.migrate(), isTrue);
910+
await migration.migrate();
910911
expect(xcodeProjectInfoFile.existsSync(), isFalse);
911912

912913
expect(testLogger.traceText, contains('Xcode project not found, skipping removing bitcode migration'));
@@ -922,7 +923,7 @@ platform :ios, '12.0'
922923
project,
923924
testLogger,
924925
);
925-
expect(await migration.migrate(), isTrue);
926+
await migration.migrate();
926927

927928
expect(xcodeProjectInfoFile.lastModifiedSync(), projectLastModified);
928929
expect(xcodeProjectInfoFile.readAsStringSync(), xcodeProjectInfoFileContents);
@@ -943,7 +944,7 @@ platform :ios, '12.0'
943944
project,
944945
testLogger,
945946
);
946-
expect(await migration.migrate(), isTrue);
947+
await migration.migrate();
947948

948949
expect(xcodeProjectInfoFile.readAsStringSync(), '''
949950
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
@@ -1408,6 +1409,104 @@ LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frame
14081409
expect(testLogger.statusText, contains('Adding input path to Thin Binary build phase.'));
14091410
});
14101411
});
1412+
1413+
group('migrate @UIApplicationMain attribute to @main', () {
1414+
late MemoryFileSystem memoryFileSystem;
1415+
late BufferLogger testLogger;
1416+
late FakeIosProject project;
1417+
late File appDelegateFile;
1418+
1419+
setUp(() {
1420+
memoryFileSystem = MemoryFileSystem();
1421+
testLogger = BufferLogger.test();
1422+
project = FakeIosProject();
1423+
appDelegateFile = memoryFileSystem.file('AppDelegate.swift');
1424+
project.appDelegateSwift = appDelegateFile;
1425+
});
1426+
1427+
testWithoutContext('skipped if files are missing', () async {
1428+
final UIApplicationMainDeprecationMigration migration = UIApplicationMainDeprecationMigration(
1429+
project,
1430+
testLogger,
1431+
);
1432+
await migration.migrate();
1433+
expect(appDelegateFile.existsSync(), isFalse);
1434+
1435+
expect(testLogger.statusText, isEmpty);
1436+
});
1437+
1438+
testWithoutContext('skipped if nothing to upgrade', () async {
1439+
const String appDelegateContents = '''
1440+
import Flutter
1441+
import UIKit
1442+
1443+
@main
1444+
@objc class AppDelegate: FlutterAppDelegate {
1445+
override func application(
1446+
_ application: UIApplication,
1447+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
1448+
) -> Bool {
1449+
GeneratedPluginRegistrant.register(with: self)
1450+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1451+
}
1452+
}
1453+
''';
1454+
appDelegateFile.writeAsStringSync(appDelegateContents);
1455+
final DateTime lastModified = appDelegateFile.lastModifiedSync();
1456+
1457+
final UIApplicationMainDeprecationMigration migration = UIApplicationMainDeprecationMigration(
1458+
project,
1459+
testLogger,
1460+
);
1461+
await migration.migrate();
1462+
1463+
expect(appDelegateFile.lastModifiedSync(), lastModified);
1464+
expect(appDelegateFile.readAsStringSync(), appDelegateContents);
1465+
1466+
expect(testLogger.statusText, isEmpty);
1467+
});
1468+
1469+
testWithoutContext('updates AppDelegate.swift', () async {
1470+
appDelegateFile.writeAsStringSync('''
1471+
import Flutter
1472+
import UIKit
1473+
1474+
@UIApplicationMain
1475+
@objc class AppDelegate: FlutterAppDelegate {
1476+
override func application(
1477+
_ application: UIApplication,
1478+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
1479+
) -> Bool {
1480+
GeneratedPluginRegistrant.register(with: self)
1481+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1482+
}
1483+
}
1484+
''');
1485+
1486+
final UIApplicationMainDeprecationMigration migration = UIApplicationMainDeprecationMigration(
1487+
project,
1488+
testLogger,
1489+
);
1490+
await migration.migrate();
1491+
1492+
expect(appDelegateFile.readAsStringSync(), '''
1493+
import Flutter
1494+
import UIKit
1495+
1496+
@main
1497+
@objc class AppDelegate: FlutterAppDelegate {
1498+
override func application(
1499+
_ application: UIApplication,
1500+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
1501+
) -> Bool {
1502+
GeneratedPluginRegistrant.register(with: self)
1503+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1504+
}
1505+
}
1506+
''');
1507+
expect(testLogger.warningText, contains('uses the deprecated @UIApplicationMain attribute, updating'));
1508+
});
1509+
});
14111510
}
14121511

14131512
class FakeIosProject extends Fake implements IosProject {
@@ -1439,6 +1538,9 @@ class FakeIosProject extends Fake implements IosProject {
14391538

14401539
@override
14411540
Directory podRunnerTargetSupportFiles = MemoryFileSystem.test().directory('Pods-Runner');
1541+
1542+
@override
1543+
File appDelegateSwift = MemoryFileSystem.test().file('AppDelegate.swift');
14421544
}
14431545

14441546
class FakeIOSMigrator extends ProjectMigrator {

0 commit comments

Comments
 (0)