You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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不一致。

问题原因

  1. 过早触发View加载
    你的lazy var button在初始化时直接访问view.frame,这会强制ViewController的view提前加载(早于viewDidLoad阶段)。当当前ViewController的视图处于活跃显示状态时,UIKit会因为视图正在交互,拒绝执行移除当前VC的栈替换操作,转而将新VC push到原有栈中,导致原VC残留。

  2. 数组切片隐式转换的潜在问题
    copyVCS.dropLast()返回的是ArraySlice<UIViewController>类型,虽然Swift会隐式将其转换为Array,但这种转换可能导致操作的数组并非完全独立的实例,进而影响导航栈的更新逻辑。

  3. 即时打印的时机误差
    调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 23:15:41