UINavigationController中setViewControllers方法异常行为原因排查
导航栈调用
setViewControllers后与预期不一致的问题分析 问题场景
测试导航栈替换逻辑时,编写了如下代码:
ViewController 实现
class ViewController: UIViewController { lazy var button = UIButton(frame: CGRect(x: view.frame.width/2-100, y: view.frame.height/2-25, width: 200, height: 50)) override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white title = "ViewController" view.addSubview(button) button.configuration = .tinted() button.configuration?.title = "Click me" button.configuration?.baseBackgroundColor = .systemPink button.configuration?.baseForegroundColor = .systemPink button.addTarget(self, action: #selector(click), for: .touchUpInside) } }
按钮点击方法
@objc func click() { let newVC = ViewController() newVC.title = "ViewController 2" newVC.view.backgroundColor = .systemCyan var copyVCS = self.navigationController!.viewControllers print("\n-----Copied Stack------\n\(copyVCS)") copyVCS = copyVCS.dropLast() copyVCS.append(newVC) print("\n-----Mutated Stack------\n\(copyVCS)") self.navigationController!.setViewControllers(copyVCS, animated: true) print("\n-----New Navigation Stack-----\n\(navigationController!.viewControllers)") }
调用setViewControllers(copyVCS, animated: true)后,控制台输出显示原本应该被移除的ViewController仍留在导航栈首位,实际栈与传入的copyVCS不一致。
问题原因
过早触发View加载
你的lazy var button在初始化时直接访问view.frame,这会强制ViewController的view提前加载(早于viewDidLoad阶段)。当当前ViewController的视图处于活跃显示状态时,UIKit会因为视图正在交互,拒绝执行移除当前VC的栈替换操作,转而将新VC push到原有栈中,导致原VC残留。数组切片隐式转换的潜在问题
copyVCS.dropLast()返回的是ArraySlice<UIViewController>类型,虽然Swift会隐式将其转换为Array,但这种转换可能导致操作的数组并非完全独立的实例,进而影响导航栈的更新逻辑。即时打印的时机误差
调用setViewControllers后立刻打印导航栈,此时动画尚未完成,UIKit的栈更新操作还未彻底完成,打印结果并非最终状态。
解决方案
1. 延迟设置按钮Frame,避免提前加载View
将按钮的Frame设置移到viewDidLayoutSubviews方法中,此时View的布局已完成,Frame值准确,且不会提前触发View加载:
class ViewController: UIViewController { lazy var button = UIButton() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white title = "ViewController" view.addSubview(button) button.configuration = .tinted() button.configuration?.title = "Click me" button.configuration?.baseBackgroundColor = .systemPink button.configuration?.baseForegroundColor = .systemPink button.addTarget(self, action: #selector(click), for: .touchUpInside) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() button.frame = CGRect(x: view.frame.width/2-100, y: view.frame.height/2-25, width: 200, height: 50) } }
2. 显式转换数组切片为数组
确保操作的是独立的数组实例,避免隐式转换带来的问题:
@objc func click() { let newVC = ViewController() newVC.title = "ViewController 2" newVC.view.backgroundColor = .systemCyan var copyVCS = self.navigationController!.viewControllers print("\n-----Copied Stack------\n\(copyVCS)") // 显式转换为Array,确保操作独立实例 copyVCS = Array(copyVCS.dropLast()) copyVCS.append(newVC) print("\n-----Mutated Stack------\n\(copyVCS)") self.navigationController!.setViewControllers(copyVCS, animated: true) // 延迟打印,等待动画完成后获取最终栈状态 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { print("\n-----New Navigation Stack-----\n\(self.navigationController!.viewControllers)") } }
3. 直接构造目标栈(可选优化)
如果需求是替换最后一个VC为新VC,可以直接构造目标数组,简化操作:
@objc func click() { let newVC = ViewController() newVC.title = "ViewController 2" newVC.view.backgroundColor = .systemCyan let targetStack = Array(navigationController!.viewControllers.dropLast()) + [newVC] navigationController!.setViewControllers(targetStack, animated: true) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { print("\n-----New Navigation Stack-----\n\(self.navigationController!.viewControllers)") } }
内容的提问来源于stack exchange,提问作者Nathace
相关产品推荐
相关产品推荐

