基于NSUserActivity的UITabViewController(含导航控制器)状态恢复问询
基于NSUserActivity的Scene状态恢复方案
针对疑问的解答
1. 导航栈中视图控制器的恢复逻辑与时机
NSUserActivity不会自动感知导航栈,状态恢复的核心逻辑需要主动实现:
- 时机:当Scene重新激活(比如APP从后台恢复、CarPlay切换连接)时,系统会在
SceneDelegate的scene(_:willConnectTo:options:)方法中传入保存的NSUserActivity,此时就是触发状态恢复的时机。 - 识别目标控制器:保存状态时,需把导航栈中每个控制器的标识(如类名、自定义ID)和必要参数存入
NSUserActivity的userInfo字典;恢复时读取这些信息,逐个实例化控制器并重建导航栈。另外UINavigationController本身支持自动保存栈结构,但需要给每个控制器设置唯一的restorationIdentifier,不过自定义userInfo的方式更灵活,适合复杂层级。
2. 确保恢复的控制器为正确类
可通过两种方式保障:
- 保存状态时,将控制器的类名(通过
NSStringFromClass(type(of: self))获取)存入NSUserActivity的userInfo;恢复时用NSClassFromString解析类名,实例化对应的控制器类。 - 给每个可恢复的控制器设置
restorationClass属性,指定恢复时使用的类,系统会自动根据该属性创建正确的实例。
基于UITabBarController+UINavigationController的简单示例
1. SceneDelegate核心代码
负责状态的保存与触发恢复:
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) // 初始化根TabBar控制器 let tabBarVC = MainTabBarController() window?.rootViewController = tabBarVC window?.makeKeyAndVisible() // 处理传入的NSUserActivity,执行状态恢复 if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { restoreAppState(from: activity) } } // 生成保存状态的NSUserActivity func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { guard let tabBarVC = window?.rootViewController as? MainTabBarController else { return nil } let activity = NSUserActivity(activityType: "com.yourapp.state.restoration") // 保存当前选中的Tab索引 activity.userInfo?["selectedTabIndex"] = tabBarVC.selectedIndex // 保存当前Tab下的导航栈信息 if let navVC = tabBarVC.selectedViewController as? UINavigationController { var stackData = [[String: Any]]() for vc in navVC.viewControllers { if let restorableVC = vc as? StateRestorable { stackData.append(restorableVC.encodeState()) } } activity.userInfo?["navigationStack"] = stackData } return activity } // 从NSUserActivity恢复应用状态 private func restoreAppState(from activity: NSUserActivity) { guard let tabBarVC = window?.rootViewController as? MainTabBarController, let userInfo = activity.userInfo else { return } // 恢复选中的Tab if let index = userInfo["selectedTabIndex"] as? Int { tabBarVC.selectedIndex = index } // 重建导航栈 if let navVC = tabBarVC.selectedViewController as? UINavigationController, let stackData = userInfo["navigationStack"] as? [[String: Any]] { var restoredVCs = [UIViewController]() for data in stackData { guard let className = data["className"] as? String, let vcClass = NSClassFromString(className) as? StateRestorable.Type else { continue } let vc = vcClass.init() vc.restoreState(from: data) restoredVCs.append(vc) } if !restoredVCs.isEmpty { navVC.setViewControllers(restoredVCs, animated: false) } } } }
2. 定义可恢复协议
统一控制器的状态编码与恢复接口:
protocol StateRestorable: UIViewController { func encodeState() -> [String: Any] func restoreState(from data: [String: Any]) } // 扩展协议,提供默认实现 extension StateRestorable { func encodeState() -> [String: Any] { return ["className": NSStringFromClass(type(of: self))] } func restoreState(from data: [String: Any]) { // 子类可重写此方法,恢复自定义数据 } }
3. 实现TabBar控制器
配置Tab与对应的导航控制器:
class MainTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() // 首页Tab let homeNav = UINavigationController(rootViewController: HomeViewController()) homeNav.tabBarItem = UITabBarItem(title: "首页", image: UIImage(systemName: "house"), tag: 0) // 我的Tab let profileNav = UINavigationController(rootViewController: ProfileViewController()) profileNav.tabBarItem = UITabBarItem(title: "我的", image: UIImage(systemName: "person"), tag: 1) viewControllers = [homeNav, profileNav] } }
4. 具体页面控制器实现
// 首页控制器 class HomeViewController: UIViewController, StateRestorable { var userData: String? override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white title = "首页" // 添加跳转按钮 let pushBtn = UIButton(type: .system) pushBtn.setTitle("查看详情", for: .normal) pushBtn.addTarget(self, action: #selector(pushDetail), for: .touchUpInside) pushBtn.frame = CGRect(x: 100, y: 200, width: 200, height: 44) view.addSubview(pushBtn) } @objc func pushDetail() { let detailVC = DetailViewController() detailVC.detailContent = "首页专属详情内容" navigationController?.pushViewController(detailVC, animated: true) } // 重写编码方法,保存自定义数据 func encodeState() -> [String: Any] { var state = super.encodeState() state["userData"] = userData ?? "" return state } func restoreState(from data: [String: Any]) { userData = data["userData"] as? String } } // 详情页控制器 class DetailViewController: UIViewController, StateRestorable { var detailContent: String? override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .lightGray title = "详情页" if let content = detailContent { let label = UILabel(frame: CGRect(x: 50, y: 200, width: 300, height: 44)) label.text = content view.addSubview(label) } } func encodeState() -> [String: Any] { var state = super.encodeState() state["detailContent"] = detailContent ?? "" return state } func restoreState(from data: [String: Any]) { detailContent = data["detailContent"] as? String } } // 我的页面控制器 class ProfileViewController: UIViewController, StateRestorable { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .blue title = "我的" } }
注意事项
- 在Info.plist中注册
NSUserActivityTypes,添加你使用的activityType(如com.yourapp.state.restoration) - 给Scene配置
restorationIdentifier,可在Info.plist的UIApplicationSceneManifest中设置,或代码中通过scene.session.configuration.restorationIdentifier指定
内容的提问来源于stack exchange,提问作者Nic Hubbard
相关产品推荐
相关产品推荐

