Skip to content

Commit

Permalink
[iOS] Migrate @UIApplicationMain attribute to @main (#146707)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
loic-sharma authored Apr 16, 2024
1 parent 5a0369d commit 194fefa
Show file tree
Hide file tree
Showing 14 changed files with 173 additions and 20 deletions.
2 changes: 1 addition & 1 deletion dev/a11y_assessments/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import Flutter
import UIKit

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
2 changes: 1 addition & 1 deletion dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import Flutter
import UIKit

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import Flutter
import UIKit

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import Flutter
import UIKit

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
2 changes: 1 addition & 1 deletion dev/manual_tests/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import Flutter
import UIKit

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
2 changes: 1 addition & 1 deletion examples/api/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import UIKit
import Flutter

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ enum MyFlutterErrorCode {
static let unavailable = "UNAVAILABLE"
}

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {
private var eventSink: FlutterEventSink?

Expand Down
2 changes: 2 additions & 0 deletions packages/flutter_tools/lib/src/ios/mac.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
10 changes: 5 additions & 5 deletions packages/flutter_tools/lib/src/xcode_project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Flutter
import UIKit

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'));
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 194fefa

Please sign in to comment.