blob: ae16cf9bc8d843bf254b8e5b0f8377db58f02ba3 [file] [log] [blame]
// 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 'package:meta/meta.dart';
import '../base/io.dart';
import '../base/project_migrator.dart';
import '../ios/plist_parser.dart';
import '../xcode_project.dart';
/// Migrates Xcode's Info.plist and AppDelegate to support UIScene if matches original templates.
/// Otherwise, provides link to documentation to migrate manually.
///
/// Only migrates if
class UISceneMigration extends ProjectMigrator {
UISceneMigration(
IosProject project,
super.logger, {
required bool isMigrationFeatureEnabled,
required PlistParser plistParser,
}) : _isMigrationFeatureEnabled = isMigrationFeatureEnabled,
_project = project,
_plistParser = plistParser;
final bool _isMigrationFeatureEnabled;
final PlistParser _plistParser;
final IosProject _project;
@visibleForTesting
static const originalSwiftAppDelegate = '''
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)
}
}
''';
static const secondaryOriginalSwiftAppDelegate = '''
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterPluginRegistrant {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
pluginRegistrant = self
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func register(with registry: any FlutterPluginRegistry) {
GeneratedPluginRegistrant.register(with: registry)
}
}
''';
@visibleForTesting
static const originalObjCAppDelegateHeader = '''
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate
@end
''';
@visibleForTesting
static const originalObjCAppDelegateImplementation = '''
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
''';
@visibleForTesting
static const newObjCAppDelegateHeader = '''
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate <FlutterImplicitEngineDelegate>
@end
''';
@visibleForTesting
static const newObjCAppDelegateImplementation = '''
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (void)didInitializeImplicitFlutterEngine:(NSObject<FlutterImplicitEngineBridge>*)engineBridge {
[GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry];
}
@end
''';
@visibleForTesting
static const newSwiftAppDelegate = '''
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
''';
@override
Future<void> migrate() async {
if (!_isMigrationFeatureEnabled) {
return;
}
// If we can't find their Info.plist, they will need to migrate manually and update the config
// to no longer see this warning.
if (!_project.defaultHostInfoPlist.existsSync()) {
logger.printTrace('UIScene migration: unable to find Info.plist');
_printErrorMessage(withConfigInstructions: true);
return;
}
// Consider it already migrated if the Info.plist has UIApplicationSceneManifest settings.
final String originalInfoPlist = _project.defaultHostInfoPlist.readAsStringSync();
if (originalInfoPlist.contains('UIApplicationSceneManifest')) {
return;
}
// If UIMainStoryboardFile is missing or not "Main", don't auto-migrate.
try {
final String? storyboardName = _plistParser.getValueFromFile<String>(
_project.defaultHostInfoPlist.path,
'UIMainStoryboardFile',
);
if (storyboardName == null || storyboardName != 'Main') {
logger.printTrace('UIScene migration: unable to find matching storyboard');
_printErrorMessage();
return;
}
} on ProcessException {
logger.printTrace('UIScene migration: unable to find matching storyboard');
_printErrorMessage();
return;
}
final bool autoMigratedAppDelegate = _migrateAppDelegate();
var autoMigratedInfoPlist = false;
if (autoMigratedAppDelegate) {
autoMigratedInfoPlist = _migrateInfoPlist();
}
if (!autoMigratedAppDelegate || !autoMigratedInfoPlist) {
_printErrorMessage();
return;
}
logger.printStatus(
'Finished migration to UIScene lifecycle. See https://flutter.dev/to/uiscene-migration for details.',
);
}
bool _migrateAppDelegate() {
if (_project.appDelegateSwift.existsSync()) {
final String projectAppDelegate = _project.appDelegateSwift.readAsStringSync().trim();
if (projectAppDelegate == originalSwiftAppDelegate.trim() ||
projectAppDelegate == secondaryOriginalSwiftAppDelegate.trim()) {
_project.appDelegateSwift.writeAsStringSync(newSwiftAppDelegate);
return true;
}
logger.printTrace('UIScene migration: AppDelegate does not match original template.');
return false;
} else if (_project.appDelegateObjcImplementation.existsSync() &&
_project.appDelegateObjcHeader.existsSync()) {
final String projectAppDelegateImplementation = _project.appDelegateObjcImplementation
.readAsStringSync()
.trim();
final String projectAppDelegateHeader = _project.appDelegateObjcHeader
.readAsStringSync()
.trim();
if (projectAppDelegateImplementation == originalObjCAppDelegateImplementation.trim() &&
projectAppDelegateHeader == originalObjCAppDelegateHeader.trim()) {
_project.appDelegateObjcImplementation.writeAsStringSync(newObjCAppDelegateImplementation);
_project.appDelegateObjcHeader.writeAsStringSync(newObjCAppDelegateHeader);
return true;
}
logger.printTrace('UIScene migration: AppDelegate does not match original template.');
return false;
}
logger.printTrace('UIScene migration: unable to find AppDelegate');
return false;
}
bool _migrateInfoPlist() {
if (_plistParser.insertKeyWithJson(
_project.defaultHostInfoPlist.path,
key: 'UIApplicationSceneManifest',
json: '''
{
"UIApplicationSupportsMultipleScenes": false,
"UISceneConfigurations": {
"UIWindowSceneSessionRoleApplication": [{
"UISceneClassName": "UIWindowScene",
"UISceneDelegateClassName": "FlutterSceneDelegate",
"UISceneConfigurationName": "flutter",
"UISceneStoryboardFile": "Main"
}]
}
}''',
)) {
return true;
}
logger.printTrace('UIScene migration: unable to insert into Info.plist');
return false;
}
void _printErrorMessage({bool withConfigInstructions = false}) {
final buffer = StringBuffer();
buffer.writeln(
'To ensure your app continues to launch on upcoming iOS versions, UIScene lifecycle '
'support will soon be required. Please see https://flutter.dev/to/uiscene-migration '
'for the migration guide.',
);
if (withConfigInstructions) {
buffer.writeln(
'See https://flutter.dev/to/uiscene-migration/#hide-migration-warning for instructions to '
'hide this warning.',
);
}
logger.printError(buffer.toString());
}
}