Swift 实现同一容器内基于Nib的子视图控制器切换方案
多Nib子控制器容器切换与交互实现方案
你现有代码的核心逻辑缺陷:
- 没有打通子控制器与容器的交互链路,容器拿不到子按钮的点击事件
- 违反UIKit容器控制器规范,提前加载所有子控制器视图、仅手动添加首个子视图,没有维护页面切换的状态
- 硬访问子控制器内部控件会造成强耦合,长期维护性差
1. 解耦交互:通过代理打通子控制器与容器的通信
不要让容器直接读取子控制器的内部nextButton,定义通用的交互协议,让每个步骤子控制器主动把点击事件抛给容器:
// 定义统一的步骤页交互协议 protocol StepViewControllerDelegate: AnyObject { /// 子控制器触发"下一步"事件 func stepViewControllerDidTapNext(_ stepVC: UIViewController) /// 子控制器触发"返回上一步"事件 func stepViewControllerDidTapBack(_ stepVC: UIViewController) }
给三个独立的Step子控制器(StepOne/StepTwo/StepThree)统一增加弱引用代理属性,将原有关联在nextButton上的IBAction逻辑替换为抛出代理事件:
class StepOneViewController: UIViewController { // 弱引用持有代理,避免循环引用 weak var delegate: StepViewControllerDelegate? @IBOutlet private(set) weak var nextButton: UIButton! // 原有nextButton关联的IBAction @IBAction func nextButtonTapped(_ sender: UIButton) { delegate?.stepViewControllerDidTapNext(self) } }
硬访问子控制器内部控件的问题:一旦子控制器内部按钮命名调整、样式变更,甚至某一步骤页没有next按钮,容器层代码会直接触发空值崩溃。代理方式下,子控制器内部逻辑完全自闭环,容器不需要关心子控件的实现细节,仅响应约定好的交互事件即可。
2. 补全容器的页面切换核心逻辑
在ContainerViewController中增加状态维护变量,替换原有提前加载所有子视图的逻辑:
class ContainerViewController: UIViewController { // 当前展示的步骤索引 private var currentStepIndex = 0 // 当前正在展示的子控制器引用 private var currentDisplayedVC: UIViewController? var stepList = [UIViewController]() // 原有outlets部分保持不变:logoImageview、titleLabel、progressStackView }
重写setupControllers方法,完成子控制器初始化、代理绑定、首页面加载:
private func setupControllers() { // 初始化三个Nib子控制器 let step1 = StepOneViewController(nibName: "StepOneViewController", bundle: nil) let step2 = StepTwoViewController(nibName: "StepTwoViewController", bundle: nil) let step3 = StepThreeViewController(nibName: "StepThreeViewController", bundle: nil) // 统一为所有子控制器设置代理 [step1, step2, step3].forEach { $0.delegate = self } stepList = [step1, step2, step3] // 加载首个展示页面 showStep(at: currentStepIndex) }
实现通用的页面切换方法,严格遵循UIKit容器控制器的生命周期规范,完全复用你现有的布局约束逻辑:
private func showStep(at index: Int) { // 边界判断,防止数组越界 guard index >= 0, index < stepList.count else { return } // 更新当前步骤索引 currentStepIndex = index let targetVC = stepList[index] // 移除当前正在展示的旧控制器 if let oldVC = currentDisplayedVC { oldVC.willMove(toParent: nil) oldVC.view.removeFromSuperview() oldVC.removeFromParent() } // 将新控制器加入容器层级 addChild(targetVC) // 复用你原有写好的布局约束逻辑 configureStepViewsLayout(onStepView: targetVC.view) targetVC.didMove(toParent: self) // 更新当前展示的控制器引用 currentDisplayedVC = targetVC // 切页完成后,同步更新进度条样式 updateProgressStack(for: index) }
你原有的configureStepViewsLayout方法不需要做任何修改,可以直接复用,因为传入的目标视图就是要展示的子控制器视图,和原有布局逻辑完全兼容。
3. 接收子控制器交互事件,实现切页动作
让ContainerViewController遵守StepViewControllerDelegate协议,实现代理方法,接住子控制器抛出的点击事件:
extension ContainerViewController: StepViewControllerDelegate { func stepViewControllerDidTapNext(_ stepVC: UIViewController) { // 点击下一步时,索引+1,触发切页 showStep(at: currentStepIndex + 1) } func stepViewControllerDidTapBack(_ stepVC: UIViewController) { // 点击返回时,索引-1,触发切页 showStep(at: currentStepIndex - 1) } }
4. 实现进度条联动更新
在updateProgressStack方法中,根据当前的步骤索引同步调整progressStackView的样式,这个方法会在每次切页完成后自动触发:
private func updateProgressStack(for stepIndex: Int) { // 遍历stackView的每个子视图,对应每个步骤的指示器 progressStackView.arrangedSubviews.enumerated().forEach { (index, indicatorView) in if index <= stepIndex { // 已完成/当前所在步骤,设为主题色 indicatorView.backgroundColor = .systemBlue } else { // 未到达的步骤,设为浅灰色 indicatorView.backgroundColor = .lightGray } } // 可同步更新公共标题的文案 titleLabel.text = "第\(stepIndex + 1)步 / 共\(stepList.count)步" }
关键注意事项
- 不要提前访问所有子控制器的
view属性(即原代码中提前取stepOneView/stepTwoView/stepThreeView的操作),UIKit的控制器视图是懒加载的,仅在需要展示时再触发视图加载,可避免多余视图叠加、内存浪费的问题。 - 所有子控制器的代理必须用
weak修饰,否则会出现“容器持有子控制器、子控制器持有容器代理”的循环引用,造成内存泄漏。 - 切页时必须完整走容器控制器的生命周期方法(
addChild、willMove(toParent:)、removeFromParent、didMove(toParent:)),否则会出现子控制器事件响应异常、内存无法回收、旋转适配失效的问题。
内容的提问来源于stack exchange,提问作者3rnestocs
相关产品推荐
相关产品推荐

