如何从UIScrollView分页子控制器中Push新UIViewController?
解决UIScrollView子控制器中Push新页面的问题
这个问题很常见——你现在的核心问题是没有正确将子控制器添加到父控制器的生命周期管理中,只是单纯把视图塞到了UIScrollView里,导致子控制器的navigationController属性为nil,自然没法执行push操作。我给你一步步梳理解决方案:
首先,修正子控制器的添加方式
iOS中,控制器的视图被添加到其他视图时,必须同时将控制器本身添加为父控制器的子控制器,这样系统才会正确管理它的生命周期,并且子控制器才能通过parent属性关联到父控制器,进而拿到导航栈。
我们把子控制器的创建和管理逻辑移到DadController中(而不是DadView),这样职责更清晰:
class DadController: UIViewController { var dadView: DadView! // 持有子控制器引用,避免被提前释放 private var childVCs: [UIViewController] = [] override func loadView() { super.loadView() dadView = DadView() view = dadView } override func viewDidLoad() { super.viewDidLoad() setupChildViewControllers() } private func setupChildViewControllers() { // 创建第一个子控制器VC1 let vc1 = VC1() // 1. 添加为子控制器 addChild(vc1) // 2. 添加视图到scrollView dadView.scrollView.addSubview(vc1.view) vc1.view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height) // 3. 通知子控制器已完成添加 vc1.didMove(toParent: self) childVCs.append(vc1) // 可以继续添加更多子控制器,比如VC2 let vc2 = UIViewController() vc2.view.backgroundColor = .orange addChild(vc2) vc2.view.frame = CGRect(x: view.bounds.width, y: 0, width: view.bounds.width, height: view.bounds.height) dadView.scrollView.addSubview(vc2.view) vc2.didMove(toParent: self) childVCs.append(vc2) // 设置scrollView的内容大小,适配分页 dadView.scrollView.contentSize = CGSize( width: view.bounds.width * CGFloat(childVCs.count), height: view.bounds.height ) } }
然后,在VC1中添加按钮并实现Push逻辑
现在VC1已经是DadController的子控制器了,我们可以通过两种方式实现push:
方式一:直接通过父控制器获取导航控制器(简单直接)
在VC1里添加按钮,点击时通过parent?.navigationController拿到导航栈,然后push新页面:
class VC1: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .green // 创建并添加Push按钮 let pushBtn = UIButton(type: .system) pushBtn.setTitle("Push New Page", for: .normal) pushBtn.titleLabel?.font = .systemFont(ofSize: 18) pushBtn.addTarget(self, action: #selector(onPushTap), for: .touchUpInside) pushBtn.translatesAutoresizingMaskIntoConstraints = false view.addSubview(pushBtn) // 布局按钮到屏幕中心 NSLayoutConstraint.activate([ pushBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor), pushBtn.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) } @objc private func onPushTap() { // 从父控制器链中获取导航控制器 guard let navController = parent?.navigationController else { print("无法获取导航控制器,请确认DadController在导航栈中") return } // 创建新的控制器并push let newVC = UIViewController() newVC.view.backgroundColor = .blue newVC.title = "New Page" navController.pushViewController(newVC, animated: true) } }
方式二:使用代理模式(更解耦,推荐复杂场景)
如果你的子控制器需要和父控制器交互更多逻辑,用代理模式可以避免强耦合:
- 先在VC1中定义代理协议:
protocol VC1Delegate: AnyObject { func vc1DidRequestPushNewPage(_ vc: VC1) } class VC1: UIViewController { // 弱引用代理,避免循环引用 weak var delegate: VC1Delegate? // ... 按钮创建和布局代码和上面一致 @objc private func onPushTap() { delegate?.vc1DidRequestPushNewPage(self) } }
- 在DadController中遵守并实现代理:
class DadController: UIViewController, VC1Delegate { // ... 其他代码 private func setupChildViewControllers() { let vc1 = VC1() vc1.delegate = self // 设置代理 addChild(vc1) // ... 其他添加代码 } // 实现代理方法 func vc1DidRequestPushNewPage(_ vc: VC1) { guard let navController = navigationController else { return } let newVC = UIViewController() newVC.view.backgroundColor = .blue newVC.title = "New Page" navController.pushViewController(newVC, animated: true) } }
最后,优化DadView的职责
建议让DadView只负责视图层的逻辑,不要创建控制器,我们可以给它加一个方法来添加子视图:
class DadView: UIView { let scrollView: UIScrollView = { let view = UIScrollView() view.isPagingEnabled = true view.showsHorizontalScrollIndicator = false return view }() override init(frame: CGRect) { super.init(frame: frame) setupScrollView() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupScrollView() { addSubview(scrollView) scrollView.translatesAutoresizingMaskIntoConstraints = false // 用AutoLayout固定scrollView到父视图四边 NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: topAnchor), scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } // 提供方法给外部添加子视图 func addChildView(_ childView: UIView, at index: Int, totalCount: Int) { childView.frame = CGRect( x: CGFloat(index) * bounds.width, y: 0, width: bounds.width, height: bounds.height ) scrollView.addSubview(childView) scrollView.contentSize = CGSize( width: bounds.width * CGFloat(totalCount), height: bounds.height ) } }
这样DadController里的setupChildViewControllers就可以改成调用这个方法:
private func setupChildViewControllers() { let vc1 = VC1() vc1.delegate = self addChild(vc1) dadView.addChildView(vc1.view, at: 0, totalCount: 2) vc1.didMove(toParent: self) childVCs.append(vc1) let vc2 = UIViewController() vc2.view.backgroundColor = .orange addChild(vc2) dadView.addChildView(vc2.view, at: 1, totalCount: 2) vc2.didMove(toParent: self) childVCs.append(vc2) }
关键总结
- 永远不要只把子控制器的视图添加到其他视图中,必须同时通过
addChild(_:)和didMove(toParent:)将控制器添加为父控制器的子控制器,确保生命周期正确管理。 - 子控制器只有在正确关联父控制器后,才能通过
parent属性访问到父控制器的导航栈,进而执行push操作。
内容的提问来源于stack exchange,提问作者yambo
相关产品推荐
相关产品推荐

