Swift使用present配合fullScreen触发不平衡转场调用错误咨询
报错原因说明
.fullScreen 模态样式转场完成后自动移除底层presenting视图的内置行为不是触发unbalanced calls to begin/end appearance transitions警告的根本原因。
这个警告的本质是UIKit检测到视图控制器的生命周期转场调用不成对:也就是某一轮viewWillAppear/viewDidAppear、viewWillDisappear/viewDidDisappear的调用序列被打断,通常是因为你在当前控制器还没完成自身加载/上一轮转场时,就触发了新的模态弹出操作。
你遇到的场景属于典型的启动时序问题:应用启动时mainView作为根控制器还没走完自身的viewDidAppear流程,你就立刻调用present弹出loginView。.fullScreen样式触发弹出时会立刻给底层的mainView发送viewWillDisappear事件,此时mainView自己的viewDidAppear还没执行完,就会出现转场调用不匹配的警告。而.overFullScreen样式不会触发底层视图的disappear生命周期,所以不会撞上这个时序问题,这也是换样式后警告消失的原因。
另外你当前的topViewController获取逻辑在iOS13+多场景环境下有缺陷:启动阶段UIApplication.shared.keyWindow可能还没完成挂载,拿到的控制器状态不正确,也会加剧转场时序问题。
可行解决方案
按优先级推荐以下方案,都可以在保留.fullScreen样式的前提下消除警告:
- 方案1:调整present调用时机,等控制器处于空闲状态再执行弹出
不要在根控制器的viewDidLoad、viewWillAppear方法里直接调用present,把弹出逻辑放到viewDidAppear中执行;如果是启动即弹的场景,可以把present逻辑派发到主线程下一轮循环执行,等当前启动转场全部完成后再触发:DispatchQueue.main.async { // 此时主线程上排队的启动阶段转场任务已经全部执行完 presentView(viewInstance: loginView) } // 极端场景下如果还有警告,可以加0.1s的延迟兜底 // DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // presentView(viewInstance: loginView) // } - 方案2:修正
topViewController的获取逻辑,适配iOS13+多场景规则,避免拿到状态异常的控制器
替换你现有的UIApplication扩展为以下实现:extension UIApplication { class func topViewController(controller: UIViewController? = nil) -> UIViewController? { let rootController = controller ?? { if #available(iOS 13.0, *) { let activeScene = connectedScenes .filter { $0.activationState == .foregroundActive } .first as? UIWindowScene return activeScene?.windows.first(where: \.isKeyWindow)?.rootViewController } else { return keyWindow?.rootViewController } }() guard let root = rootController else { return nil } if let nav = root as? UINavigationController { return topViewController(controller: nav.visibleViewController) } if let tab = root as? UITabBarController, let selected = tab.selectedViewController { return topViewController(controller: selected) } if let presented = root.presentedViewController { return topViewController(controller: presented) } return root } } - 方案3:启动阶段切换首个视图直接替换
rootViewController,避免模态转场的时序问题
如果loginView是启动后要显示的首个页面,完全不需要用present的方式弹出,直接替换window的根控制器即可,性能更高也不会有转场冲突:guard let window = UIApplication.shared.windows.first(where: \.isKeyWindow) else { return } window.rootViewController = loginView UIView.transition(with: window, duration: 0.2, options: .transitionCrossDissolve, animations: nil)
验证小技巧
调用present前可以打印当前topController的几个状态属性,如果任意一个属性为true,说明当前控制器还在转场流程中,此时调用present必然会触发警告,等所有属性都为false时再调用即可:
print(topController.isBeingPresented, topController.isBeingDismissed, topController.isMovingToParent, topController.isMovingFromParent)
内容的提问来源于stack exchange,提问作者Bobby

