diff --git a/dev/a11y_assessments/ios/Runner/AppDelegate.swift b/dev/a11y_assessments/ios/Runner/AppDelegate.swift index 36e03f7fbb..58f3ea9b92 100644 --- a/dev/a11y_assessments/ios/Runner/AppDelegate.swift +++ b/dev/a11y_assessments/ios/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift b/dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift index 36e03f7fbb..58f3ea9b92 100644 --- a/dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift +++ b/dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/dev/integration_tests/ios_app_with_extensions/ios/Runner/AppDelegate.swift b/dev/integration_tests/ios_app_with_extensions/ios/Runner/AppDelegate.swift index 36e03f7fbb..58f3ea9b92 100644 --- a/dev/integration_tests/ios_app_with_extensions/ios/Runner/AppDelegate.swift +++ b/dev/integration_tests/ios_app_with_extensions/ios/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/dev/integration_tests/ios_host_app_swift/Host/AppDelegate.swift b/dev/integration_tests/ios_host_app_swift/Host/AppDelegate.swift index 51ec64fcc2..e8cb4d2e95 100644 --- a/dev/integration_tests/ios_host_app_swift/Host/AppDelegate.swift +++ b/dev/integration_tests/ios_host_app_swift/Host/AppDelegate.swift @@ -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) diff --git a/dev/integration_tests/non_nullable/ios/Runner/AppDelegate.swift b/dev/integration_tests/non_nullable/ios/Runner/AppDelegate.swift index 36e03f7fbb..58f3ea9b92 100644 --- a/dev/integration_tests/non_nullable/ios/Runner/AppDelegate.swift +++ b/dev/integration_tests/non_nullable/ios/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/dev/manual_tests/ios/Runner/AppDelegate.swift b/dev/manual_tests/ios/Runner/AppDelegate.swift index 36e03f7fbb..58f3ea9b92 100644 --- a/dev/manual_tests/ios/Runner/AppDelegate.swift +++ b/dev/manual_tests/ios/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/examples/api/ios/Runner/AppDelegate.swift b/examples/api/ios/Runner/AppDelegate.swift index d815fed684..a60e1595a2 100644 --- a/examples/api/ios/Runner/AppDelegate.swift +++ b/examples/api/ios/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/examples/platform_channel_swift/ios/Runner/AppDelegate.swift b/examples/platform_channel_swift/ios/Runner/AppDelegate.swift index f5eadb6930..83b04716f9 100644 --- a/examples/platform_channel_swift/ios/Runner/AppDelegate.swift +++ b/examples/platform_channel_swift/ios/Runner/AppDelegate.swift @@ -19,7 +19,7 @@ enum MyFlutterErrorCode { static let unavailable = "UNAVAILABLE" } -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler { private var eventSink: FlutterEventSink? diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 454e4dc87e..9a65ba0995 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -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 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); diff --git a/packages/flutter_tools/lib/src/ios/migrations/remove_bitcode_migration.dart b/packages/flutter_tools/lib/src/ios/migrations/remove_bitcode_migration.dart index 23b157a6cf..34d9adc5c4 100644 --- a/packages/flutter_tools/lib/src/ios/migrations/remove_bitcode_migration.dart +++ b/packages/flutter_tools/lib/src/ios/migrations/remove_bitcode_migration.dart @@ -16,14 +16,12 @@ class RemoveBitcodeMigration extends ProjectMigrator { final File _xcodeProjectInfoFile; @override - Future migrate() async { + Future migrate() async { if (_xcodeProjectInfoFile.existsSync()) { processFileLines(_xcodeProjectInfoFile); } else { logger.printTrace('Xcode project not found, skipping removing bitcode migration.'); } - - return true; } @override diff --git a/packages/flutter_tools/lib/src/ios/migrations/uiapplicationmain_deprecation_migration.dart b/packages/flutter_tools/lib/src/ios/migrations/uiapplicationmain_deprecation_migration.dart new file mode 100644 index 0000000000..aca45398a5 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/migrations/uiapplicationmain_deprecation_migration.dart @@ -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 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); + } +} diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index 16defd560f..9852592fa7 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -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 pluginsSupportArmSimulator() async { diff --git a/packages/flutter_tools/templates/app_shared/ios-swift.tmpl/Runner/AppDelegate.swift b/packages/flutter_tools/templates/app_shared/ios-swift.tmpl/Runner/AppDelegate.swift index 9074fee929..626664468b 100644 --- a/packages/flutter_tools/templates/app_shared/ios-swift.tmpl/Runner/AppDelegate.swift +++ b/packages/flutter_tools/templates/app_shared/ios-swift.tmpl/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart index 9934ca2030..4970200314 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart @@ -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 {