[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. ad18797428 - This updates the iOS app template and adds a migration to replace `@UIApplicationMain` uses with `@main`. 
2. 8ecbb2f29f - I ran `flutter run` on each Flutter iOS app in this repository to verify the app migrates and launches successfully.

Part of https://github.com/flutter/flutter/issues/143044
This commit is contained in:
Loïc Sharma 2024-04-16 15:13:03 -07:00 committed by GitHub
parent 5a0369df16
commit 194fefaa53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 173 additions and 20 deletions

View File

@ -5,7 +5,7 @@
import Flutter
import UIKit
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@ -5,7 +5,7 @@
import Flutter
import UIKit
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@ -5,7 +5,7 @@
import Flutter
import UIKit
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

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

View File

@ -5,7 +5,7 @@
import Flutter
import UIKit
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@ -5,7 +5,7 @@
import Flutter
import UIKit
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@ -5,7 +5,7 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@ -19,7 +19,7 @@ enum MyFlutterErrorCode {
static let unavailable = "UNAVAILABLE"
}
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {
private var eventSink: FlutterEventSink?

View File

@ -35,6 +35,7 @@ import 'migrations/project_base_configuration_migration.dart';
import 'migrations/project_build_location_migration.dart';
import 'migrations/remove_bitcode_migration.dart';
import 'migrations/remove_framework_link_and_embedding_migration.dart';
import 'migrations/uiapplicationmain_deprecation_migration.dart';
import 'migrations/xcode_build_system_migration.dart';
import 'xcode_build_settings.dart';
import 'xcodeproj.dart';
@ -158,6 +159,7 @@ Future<XcodeBuildResult> buildXcodeProject({
XcodeScriptBuildPhaseMigration(app.project, globals.logger),
RemoveBitcodeMigration(app.project, globals.logger),
XcodeThinBinaryBuildPhaseInputPathsMigration(app.project, globals.logger),
UIApplicationMainDeprecationMigration(app.project, globals.logger),
];
final ProjectMigration migration = ProjectMigration(migrators);

View File

@ -16,14 +16,12 @@ class RemoveBitcodeMigration extends ProjectMigrator {
final File _xcodeProjectInfoFile;
@override
Future<bool> migrate() async {
Future<void> migrate() async {
if (_xcodeProjectInfoFile.existsSync()) {
processFileLines(_xcodeProjectInfoFile);
} else {
logger.printTrace('Xcode project not found, skipping removing bitcode migration.');
}
return true;
}
@override

View File

@ -0,0 +1,51 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../base/file_system.dart';
import '../../base/project_migrator.dart';
import '../../xcode_project.dart';
const String _appDelegateFileBefore = r'''
@UIApplicationMain
@objc class AppDelegate''';
const String _appDelegateFileAfter = r'''
@main
@objc class AppDelegate''';
/// Replace the deprecated `@UIApplicationMain` attribute with `@main`.
///
/// See:
/// https://github.com/apple/swift-evolution/blob/main/proposals/0383-deprecate-uiapplicationmain-and-nsapplicationmain.md
class UIApplicationMainDeprecationMigration extends ProjectMigrator {
UIApplicationMainDeprecationMigration(
IosProject project,
super.logger,
) : _appDelegateSwift = project.appDelegateSwift;
final File _appDelegateSwift;
@override
Future<void> migrate() async {
// Skip this migration if the project uses Objective-C.
if (!_appDelegateSwift.existsSync()) {
logger.printTrace(
'ios/Runner/AppDelegate.swift not found, skipping @main migration.',
);
return;
}
// Migrate the ios/Runner/AppDelegate.swift file.
final String original = _appDelegateSwift.readAsStringSync();
final String migrated = original.replaceFirst(_appDelegateFileBefore, _appDelegateFileAfter);
if (original == migrated) {
return;
}
logger.printWarning(
'ios/Runner/AppDelegate.swift uses the deprecated @UIApplicationMain attribute, updating.',
);
_appDelegateSwift.writeAsStringSync(migrated);
}
}

View File

@ -178,15 +178,15 @@ class IosProject extends XcodeBasedProject {
File get appFrameworkInfoPlist => _flutterLibRoot.childDirectory('Flutter').childFile('AppFrameworkInfo.plist');
/// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C.
File get appDelegateSwift => _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift');
File get infoPlist => _editableDirectory.childDirectory('Runner').childFile('Info.plist');
Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');
/// True, if the app project is using swift.
bool get isSwift {
final File appDelegateSwift = _editableDirectory.childDirectory('Runner').childFile('AppDelegate.swift');
return appDelegateSwift.existsSync();
}
/// True if the app project uses Swift.
bool get isSwift => appDelegateSwift.existsSync();
/// Do all plugins support arm64 simulators to run natively on an ARM Mac?
Future<bool> pluginsSupportArmSimulator() async {

View File

@ -1,7 +1,7 @@
import Flutter
import UIKit
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@ -13,6 +13,7 @@ import 'package:flutter_tools/src/ios/migrations/project_base_configuration_migr
import 'package:flutter_tools/src/ios/migrations/project_build_location_migration.dart';
import 'package:flutter_tools/src/ios/migrations/remove_bitcode_migration.dart';
import 'package:flutter_tools/src/ios/migrations/remove_framework_link_and_embedding_migration.dart';
import 'package:flutter_tools/src/ios/migrations/uiapplicationmain_deprecation_migration.dart';
import 'package:flutter_tools/src/ios/migrations/xcode_build_system_migration.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/migrations/cocoapods_script_symlink.dart';
@ -906,7 +907,7 @@ platform :ios, '12.0'
project,
testLogger,
);
expect(await migration.migrate(), isTrue);
await migration.migrate();
expect(xcodeProjectInfoFile.existsSync(), isFalse);
expect(testLogger.traceText, contains('Xcode project not found, skipping removing bitcode migration'));
@ -922,7 +923,7 @@ platform :ios, '12.0'
project,
testLogger,
);
expect(await migration.migrate(), isTrue);
await migration.migrate();
expect(xcodeProjectInfoFile.lastModifiedSync(), projectLastModified);
expect(xcodeProjectInfoFile.readAsStringSync(), xcodeProjectInfoFileContents);
@ -943,7 +944,7 @@ platform :ios, '12.0'
project,
testLogger,
);
expect(await migration.migrate(), isTrue);
await migration.migrate();
expect(xcodeProjectInfoFile.readAsStringSync(), '''
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
@ -1408,6 +1409,104 @@ LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frame
expect(testLogger.statusText, contains('Adding input path to Thin Binary build phase.'));
});
});
group('migrate @UIApplicationMain attribute to @main', () {
late MemoryFileSystem memoryFileSystem;
late BufferLogger testLogger;
late FakeIosProject project;
late File appDelegateFile;
setUp(() {
memoryFileSystem = MemoryFileSystem();
testLogger = BufferLogger.test();
project = FakeIosProject();
appDelegateFile = memoryFileSystem.file('AppDelegate.swift');
project.appDelegateSwift = appDelegateFile;
});
testWithoutContext('skipped if files are missing', () async {
final UIApplicationMainDeprecationMigration migration = UIApplicationMainDeprecationMigration(
project,
testLogger,
);
await migration.migrate();
expect(appDelegateFile.existsSync(), isFalse);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('skipped if nothing to upgrade', () async {
const String appDelegateContents = '''
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
''';
appDelegateFile.writeAsStringSync(appDelegateContents);
final DateTime lastModified = appDelegateFile.lastModifiedSync();
final UIApplicationMainDeprecationMigration migration = UIApplicationMainDeprecationMigration(
project,
testLogger,
);
await migration.migrate();
expect(appDelegateFile.lastModifiedSync(), lastModified);
expect(appDelegateFile.readAsStringSync(), appDelegateContents);
expect(testLogger.statusText, isEmpty);
});
testWithoutContext('updates AppDelegate.swift', () async {
appDelegateFile.writeAsStringSync('''
import Flutter
import UIKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
''');
final UIApplicationMainDeprecationMigration migration = UIApplicationMainDeprecationMigration(
project,
testLogger,
);
await migration.migrate();
expect(appDelegateFile.readAsStringSync(), '''
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
''');
expect(testLogger.warningText, contains('uses the deprecated @UIApplicationMain attribute, updating'));
});
});
}
class FakeIosProject extends Fake implements IosProject {
@ -1439,6 +1538,9 @@ class FakeIosProject extends Fake implements IosProject {
@override
Directory podRunnerTargetSupportFiles = MemoryFileSystem.test().directory('Pods-Runner');
@override
File appDelegateSwift = MemoryFileSystem.test().file('AppDelegate.swift');
}
class FakeIOSMigrator extends ProjectMigrator {