据说苹果要求,使用iOS 27对应的Xcode版本构建应用,必须适配UIScene,使用SceneDelegate进行创建窗口,否则无法启动。那么可能需要提前适配。
特别注意:SceneDelegate只支持iOS 13以上系统,如果存在要兼容iOS13以下系统,需要适配SceneDelegate的同时,保留AppDelegate的方式。如无特别需要,最好还是修改最低支持版本为最好。
首先说下AppDelegate与SceneDelegate的区别,以及适合哪些场景,这样才方便迁移。
AppDelegate:管理整个应用程序的生命周期;一个应用只有一个AppDelegate实例,从启动到终止,全程存在;
主要职责:
1、应用生命周期:响应应用启动、进入后台、被终止等关键事件。
2、核心系统回调:处理推送通知注册、远程控制、后台任务等。
3、初始化共享资源:配置数据库、网络框架、第三方SDK等全局组件。
4、在支持多场景时:负责创建和管理SceneDelegate 的配置;
SceneDelegate:管理单个窗口场景的生命周期。一个应用可以存在多个。
主要职责:
1、窗口界面管理:负责创建、显示和管理 UIWindow及根视图控制器。
2、场景生命周期:响应场景连接、激活、失活、进入后台、断开连接等。
3、用户界面和状态恢复:管理特定窗口的用户界面状态。
4、处理多任务:在iPadOS上,支持分屏、滑动悬浮等。
适配点:
1、创建SceneDelegate文件(附件有已创建的文件,可直接使用)
2、修改info.plist文件
直接Source Code添加以下代码
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
备注:
a、一般应用都为单窗口模式,UIApplicationSupportsMultipleScenes
要配置为NO,如果确实是多窗口模式,切记修改为YES;
b、如果项目中没有使用Main.Storyboard文件,则去掉
<key>UISceneStoryboardFile</key>
<string>Main</string>
3、在appdelegate.m中新增以下两个方法
#pragma mark - UISceneSession lifecycle
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0)){
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions API_AVAILABLE(ios(13.0)){
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
备注:Default Configuration的名称必须与第2点info.plist中UISceneConfigurationName的值一致。
4、AppDelegate中的applicationWillEnterForeground
、applicationDidBecomeActive、applicationWillResignActive、applicationDidEnterBackground
方法将不会被调用,但是该事件的通知还是会被触发。
通知依旧会被触发
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
可以将这其中的逻辑移动到sceneWillEnterForeground、sceneDidBecomeActive、sceneWillResignActive、sceneDidEnterBackground中,或者使用通知监听以上事件来执行方法;
5、[UIApplication sharedApplication].delegate.window已经获取不到window,且该属性已被屏蔽。单窗口模式由于只会生成一份SceneDelegate,其中的window和windowScene在APP生命周期中也只会存在一份,可以取巧实现一个单例,保存window与windowScene。以前使用[UIApplication sharedApplication].delegate.window或者[UIApplication sharedApplication].keyWindow的地方,直接使用单例保存的window。
6、在SceneDelegate的方法
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions API_AVAILABLE(ios(13.0));
中初始化window,创建window需要使用initWithWindowScene的方式,其中windowScene为方法中的scene,需要转换一下:
UIWindowScene *windowScene = (UIWindowScene *)scene;
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
这里创建的window在单窗口模式下,等同于AppDelegate模式中的[UIApplication sharedApplication].delegate.window。直接保存在单例中,后续使用即可。
tip:在调试过程中发现,在willConnectToSession中设置window上的广告页,即启动页-->加载在window上的广告页,偶尔会出现启动页到广告页闪一下的情况,大概率猜测是window还未创建完成,还未呈现,但又向window上添加UI导致的, 这里增加一个0.25秒的延迟即可解决,让window实际呈现后再去创建广告页。
///创建window
UIWindowScene *windowScene = (UIWindowScene *)scene;
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
self.window.backgroundColor = UIColor.white_xFFFFFF;
self.window.rootViewController = [[PNCLaunchDefaultRootVC alloc] init];
[self.window makeKeyAndVisible];
[PNCWindowSceneManager manager].keyWindow = self.window;
[PNCWindowSceneManager manager].keyWindowScene = windowScene;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self lazyInitialization];
});
7、如果存在启动广告页是通过window呈现的,初始化广告页的window时,需要使用initWithWindowScene方式初始化,单窗口直接使用单例中保存windowScene来创建即可,多窗口模式未测试过。
8、Scheme热启动适配
适配SceneDelegate后,AppDelegate中的方法:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options;
将不会被调用(网上有说通过Scheme唤起,APP如果是冷启动,该方法会被调用,但是实际调试发现,该方法始终未被调用),需要将代码逻辑迁移至SceneDelegate中:
/// 该方法只会在willConnectToSession方法执行完成后被调用,即必须有窗口成为活跃窗口才会被触发。所以通过Scheme冷启动唤起APP时,该方法不会被触发,需要按照第10点的来适配冷启动。
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts;
有区别的地方在于SceneDelegate中的URLContexts是一个元组,对应多窗口和可能存在的多次点击重复唤起场景。如果是单窗口应用,极低的概率URLContexts中存在多个值。 所以单窗口应用中,其实可以将元组当成只包含一个数据来处理。URLContexts转换为URL的方式为:
UIOpenURLContext *context = URLContexts.anyObject;
if (!context) {
return;
}
NSURL *url = context.URL;
9、Universal Link热启动适配
适配SceneDelegate后,AppDelegate中的方法:
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray<id<UIUserActivityRestoring>> * __nullable restorableObjects))restorationHandler;
将不会被调用(网上有说通过Universal Link唤起,APP如果是冷启动,该方法会被调用,但是实际调试发现,该方法始终未被调用),需要将代码逻辑迁移至SceneDelegate中,同时AppDelegate中的方法不删除,其他业务逻辑可删,直接return YES:
/// 该方法只会在willConnectToSession方法执行完成后被调用,即必须有窗口成为活跃窗口才会被触发。所以通过Universal Link冷启动唤起APP时,该方法不会被触发,需要按照第10点的来适配冷启动。
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity;
对于这两个方法,AppDelegate和SceneDelegate都是一样的,不需要特别注意,如果是多窗口模式,需要注意每个SceneDelegate的方法都会被触发。
10、Scheme、Universal Link冷启动适配
上述中有提到,SceneDelegate中的方法均在热启动的时候才会被触发,但是AppDelegate中的方法冷启动又无法被触发,这里需要单独对冷启动适配;
需要再SceneDelegate的方法中进行处理,同时AppDelegate中的方法不删除,其他业务逻辑可删,直接return YES:
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions;
该方法在窗口创建时会被调用,每个SceneDelegate中的这个方法仅会被执行一次。方法中connectionOptions参数会携带Scheme、Universal Link中的数据。使用示例如下:
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions API_AVAILABLE(ios(13.0)) {
/// 处理Scheme
if (connectionOptions.URLContexts.count > 0) {
UIOpenURLContext *context = connectionOptions.URLContexts.anyObject;
if (context.URL && [NSObject isNotEmptyString:context.URL.absoluteString]) {
/// 在这里处理逻辑,但是需要考虑window可能仍然未被成功创建,或者有广告页等延时,可以将数据存储起来,在Home页在处理相应逻辑
}
}
/// 处理Universal Link
if (connectionOptions.userActivities.count > 0) {
NSUserActivity *userActivity = connectionOptions.userActivities.anyObject;
if (userActivity && userActivity.webpageURL && [NSObject isNotEmptyString:userActivity.webpageURL.absoluteString]) {
/// 在这里处理逻辑,但是需要考虑window可能仍然未被成功创建,或者有广告页等延时,可以将数据存储起来,在Home页在处理相应逻辑
}
}
/// 继续创建窗口
}
WindowSceneManager为自定义保存window和windowScene的单例;
11、[UIApplication sharedApplication].statusBarOrientation属性已被废弃,搜索是否使用,有使用则替换成UIInterfaceOrientation orientation = [WindowSceneManager manager].keyWindowScene.interfaceOrientation;
12、[UIApplication sharedApplication].statusBarFrame属性已被废弃,搜索是否使用,有则替换成[WindowSceneManager manager].keyWindowScene.statusBarManager.statusBarFrame
13、[UIApplication sharedApplication].windows属性已被废弃,搜索是否使用,有则替换成[WindowSceneManager manager].keyWindowScene.windows
tip:要注意下UIApplication.sharedApplication.windows、UIApplication.sharedApplication.keyWindow,记得同步替换。