在 iOS 13 及以上版本中,Apple 引入了 SceneDelegate
(场景代理),核心目标是支持 多窗口功能 (iPadOS 多任务、iOS 分屏等),并将原 AppDelegate
中"UI 管理"和"场景生命周期"相关的职责拆分出来,实现更精细化的窗口控制。
一、SceneDelegate 的核心定位
SceneDelegate
与 AppDelegate
是 分工协作 的关系,二者职责边界清晰:
职责类别 | AppDelegate(应用级) | SceneDelegate(场景级) |
---|---|---|
生命周期管理 | 应用全局生命周期(启动、终止、内存警告) | 单个场景生命周期(显示、隐藏、激活、后台) |
UI 管理 | 不再负责具体窗口,仅处理全局配置 | 管理对应场景的 UIWindow 、根视图控制器 |
系统事件 | 全局事件(推送授权、后台任务全局配置) | 场景相关事件(深链接、窗口尺寸变化、状态保存) |
多窗口支持 | 不直接参与,仅通过 UISceneSession 管理场景 |
每个窗口对应一个 SceneDelegate 实例 |
二、Scene 相关基础概念
在使用 SceneDelegate
前,需先理解以下核心概念:
- Scene(场景) :对应一个独立的窗口或界面实例,例如 iPad 分屏时的两个应用窗口,每个窗口就是一个
Scene
。 - UISceneSession:场景的"配置与状态管理者",记录场景的唯一标识、配置信息、状态(激活/后台),由系统管理。
- Info.plist 配置 :iOS 13+ 应用需在
Info.plist
中声明UISceneConfiguration
,告诉系统如何创建和配置场景。
三、基础配置:开启 Scene 支持
若新建 iOS 13+ 项目,Xcode 会自动生成 SceneDelegate
文件和相关配置;若为旧项目适配,需手动完成以下步骤:
1. 添加 SceneDelegate 文件
- 新建
SceneDelegate.h
和SceneDelegate.m
(Objective-C)或SceneDelegate.swift
(Swift),默认实现如下:
Objective-C 版本
SceneDelegate.h
objc
#import <UIKit/UIKit.h>
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@property (strong, nonatomic) UIWindow * window;
@end
SceneDelegate.m
objc
#import "SceneDelegate.h"
@implementation SceneDelegate
#pragma mark - UISceneSession Lifecycle
// 场景初始化(创建窗口)
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
// 确保场景是窗口场景(排除其他类型场景)
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
// 创建窗口并绑定到当前场景
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
self.window.frame = windowScene.coordinateSpace.bounds;
// 设置根视图控制器
self.window.rootViewController = [[UIViewController alloc] init];
// 显示窗口
[self.window makeKeyAndVisible];
}
}
@end
Swift 版本
SceneDelegate.swift
swift
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
// 场景初始化(创建窗口)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// 确保场景是窗口场景
guard let windowScene = (scene as? UIWindowScene) else { return }
// 创建窗口并绑定到场景
window = UIWindow(windowScene: windowScene)
window?.frame = windowScene.coordinateSpace.bounds
// 设置根视图控制器
window?.rootViewController = UIViewController()
// 显示窗口
window?.makeKeyAndVisible()
}
}
2. 配置 Info.plist
需在 Info.plist
中添加 UISceneConfigurations
键,声明场景的配置信息(系统根据此创建场景):
- 在
Info.plist
中右键 → Add Row ,输入UISceneConfigurations
(类型为 Dictionary)。 - 在
UISceneConfigurations
下添加UIWindowSceneSessionRoleApplication
(类型为 Array),表示"应用主场景"。 - 在 Array 中添加一个 Dictionary,设置以下键:
UISceneConfigurationName
:配置名称(如Default Configuration
,自定义)。UISceneDelegateClassName
:SceneDelegate
类名(如$(PRODUCT_MODULE_NAME).SceneDelegate
)。UISceneStoryboardFile
(可选):若用 Storyboard,指定主 Storyboard 名称。
配置效果(plist 源码示例):
xml
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>YourAppName.SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
3. 修改 AppDelegate
iOS 13+ 中,AppDelegate
不再管理窗口,需删除原 window
属性和 UI 初始化代码,并添加场景会话相关方法:
Objective-C 版本(AppDelegate.m)
objc
#import "AppDelegate.h"
@implementation AppDelegate
// 应用启动完成(仅处理全局初始化,不涉及UI)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 全局配置:注册推送、初始化第三方SDK等(不创建window)
return YES;
}
// 系统请求创建新场景时调用(如用户开启多窗口)
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSession options:(UISceneConnectionOptions *)options {
// 返回 Info.plist 中配置的场景配置(匹配名称)
return [UISceneConfiguration configurationWithName:@"Default Configuration" sessionRole:connectingSession.role];
}
// 场景会话断开时调用(如用户关闭窗口)
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
// 清理场景相关资源(如释放场景专属数据)
}
@end
Swift 版本(AppDelegate.swift)
swift
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// 移除原 window 属性!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 全局初始化:注册推送、SDK等
return true
}
// 配置场景连接
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
// 场景会话断开
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// 清理资源
}
}
四、SceneDelegate 核心方法(场景生命周期)
SceneDelegate
的核心是 场景生命周期方法 ,对应单个窗口从"创建"到"销毁"的全过程,与原 AppDelegate
的 UI 生命周期方法类似,但粒度更细(每个场景独立触发)。
1. 场景初始化(连接会话)
objc
// Objective-C
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
// 1. 校验场景类型(必须是 UIWindowScene)
guard [scene isKindOfClass:[UIWindowScene class]] else { return; }
UIWindowScene *windowScene = (UIWindowScene *)scene;
// 2. 创建窗口并绑定到当前场景(关键:不再用 UIScreen.mainScreen)
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
self.window.frame = windowScene.coordinateSpace.bounds; // 用场景的坐标空间
// 3. 设置根视图控制器(示例:导航控制器包裹首页)
UIViewController *homeVC = [[UIViewController alloc] init];
homeVC.view.backgroundColor = UIColor.whiteColor;
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:homeVC];
self.window.rootViewController = nav;
// 4. 显示窗口
[self.window makeKeyAndVisible];
// 5. 处理启动参数(如通过深链接、推送启动场景)
if (connectionOptions.urlContexts.count > 0) {
NSURLContext *urlContext = connectionOptions.urlContexts.anyObject;
[self handleDeepLink:urlContext.URL]; // 自定义深链接处理
}
}
关键注意 :
iOS 13+ 中,窗口必须通过 UIWindowScene
初始化(initWithWindowScene:
),不能再用 initWithFrame:UIScreen.mainScreen.bounds
,否则多窗口场景下会出现 UI 错乱。
2. 场景激活/失活
objc
// 场景即将激活(如窗口从后台切换到前台,或新窗口被打开)
- (void)sceneDidBecomeActive:(UIScene *)scene {
// 恢复操作:重启定时器、刷新数据、恢复用户交互
}
// 场景即将失活(如窗口被切换到后台,或被其他窗口覆盖)
- (void)sceneWillResignActive:(UIScene *)scene {
// 暂停操作:暂停定时器、保存临时数据、停止动画
}
3. 场景进入/退出后台
objc
// 场景即将进入后台(如用户按 Home 键,或窗口被最小化)
- (void)sceneWillEnterForeground:(UIScene *)scene {
// 前台恢复:刷新 UI 状态、重新加载网络数据
}
// 场景已进入后台(如窗口完全被隐藏)
- (void)sceneDidEnterBackground:(UIScene *)scene {
// 后台处理:保存持久化数据、释放场景专属内存(如大图缓存)
}
4. 场景断开(销毁)
objc
// 场景即将断开会话(如用户关闭窗口,或系统回收资源)
- (void)sceneDidDisconnect:(UIScene *)scene {
// 清理操作:释放场景专属资源(如注销监听、关闭网络连接)
// 注意:此时窗口已不可见,无需处理 UI
}
五、SceneDelegate 常见场景处理
1. 处理深链接(URL Scheme/Universal Link)
iOS 13+ 中,深链接不再通过 AppDelegate
的 application:openURL:options:
处理,而是由 SceneDelegate
的以下方法接收:
objc
// 处理通过 URL 打开场景的请求
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIURLContext *> *)URLContexts {
for (UIURLContext *context in URLContexts) {
NSURL *url = context.URL;
// 解析 URL 并跳转页面(如 url = "yourapp://detail?id=123")
if ([url.scheme isEqualToString:@"yourapp"] && [url.host isEqualToString:@"detail"]) {
NSString *id = [url.query componentsSeparatedByString:@"="].lastObject;
// 跳转到详情页(需获取当前场景的根视图控制器)
UINavigationController *nav = (UINavigationController *)self.window.rootViewController;
DetailViewController *detailVC = [[DetailViewController alloc] init];
detailVC.detailId = id;
[nav pushViewController:detailVC animated:YES];
}
}
}
2. 适配多窗口(获取当前场景)
在多窗口场景下,需通过 UIScene
或 UIWindowScene
获取当前活跃的场景,避免操作错误窗口:
objc
// 方式1:从视图控制器获取当前场景
UIViewController *currentVC = self;
UIWindowScene *currentScene = currentVC.view.window.windowScene;
// 方式2:获取所有活跃的场景
NSArray<UIWindowScene *> *activeScenes = [UIApplication.sharedApplication.connectedScenes filteredArrayUsingPredicate:
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
return evaluatedObject.sceneState == UISceneStateActive;
}]
].firstObject;
// 方式3:通过 SceneSession 获取场景
UISceneSession *currentSession = self.window.windowScene.session;
NSString *sceneId = currentSession.persistentIdentifier; // 场景唯一标识
3. 场景状态保存与恢复
若需在场景销毁后恢复状态(如用户重新打开窗口时恢复之前的页面),可通过 UISceneSession
的 stateRestorationActivity
实现:
objc
// 1. 场景进入后台时保存状态
- (void)sceneDidEnterBackground:(UIScene *)scene {
// 创建保存数据的 Activity
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:@"com.yourapp.scene.restore"];
activity.userInfo = @{@"currentPage": @"detail", @"detailId": @"123"}; // 保存当前页面和参数
// 关联到场景会话
scene.session.stateRestorationActivity = activity;
}
// 2. 场景初始化时恢复状态
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
// 检查是否有恢复的 Activity
if (session.stateRestorationActivity) {
NSDictionary *userInfo = session.stateRestorationActivity.userInfo;
NSString *currentPage = userInfo[@"currentPage"];
if ([currentPage isEqualToString:@"detail"]) {
// 恢复到详情页
NSString *detailId = userInfo[@"detailId"];
// ... 跳转逻辑 ...
}
}
}
六、兼容 iOS 12 及以下版本
若应用需支持 iOS 12 及以下,需通过 版本判断 区分 AppDelegate
和 SceneDelegate
的职责:
1. AppDelegate 中兼容 UI 初始化
objc
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 全局初始化...
// iOS 12 及以下:在 AppDelegate 中创建窗口
if (@available(iOS 13.0, *)) {
// iOS 13+:由 SceneDelegate 处理 UI,此处不做操作
} else {
self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[UIViewController alloc] init]];
[self.window makeKeyAndVisible];
}
return YES;
}
2. 深链接兼容处理
objc
// AppDelegate 中(iOS 12 及以下处理深链接)
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if (@available(iOS 13.0, *)) {
return NO; // iOS 13+ 由 SceneDelegate 处理
} else {
// iOS 12 及以下处理深链接...
return YES;
}
}
// SceneDelegate 中(iOS 13+ 处理深链接)
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIURLContext *> *)URLContexts {
// ... 处理逻辑 ...
}
七、常见问题与注意事项
- 窗口初始化错误 :iOS 13+ 必须用
initWithWindowScene:
创建窗口,否则多窗口时 UI 会错乱。 - 多场景资源冲突 :每个场景的
SceneDelegate
是独立实例,需避免共享资源(如单例数据)在多场景下的状态不一致。 - SceneSession 唯一标识 :
UISceneSession.persistentIdentifier
是场景的唯一标识,可用于区分不同窗口的状态