如何通过scrollViewDidScroll根据子控制器滚动偏移调整父视图子视图Y位置?
当然可以实现!这是很常见的父子控制器联动场景
你提到的需求完全可行,核心就是让子控制器把滚动偏移的变化传递给父控制器,然后父控制器去调整自己的子视图(比如UILabel)。下面是几种常用的实现方式,你可以根据自己的项目结构选择:
方案一:自定义代理模式(推荐,耦合度低)
这是父子组件通信最规范的方式,适合需要明确通信关系的场景:
- 首先在子控制器中定义代理协议,声明滚动回调方法:
protocol ChildVCDelegate: AnyObject { func childVC(_ childVC: ChildViewController, didScrollWith offset: CGPoint) } class ChildViewController: UIViewController { // 用weak避免循环引用 weak var delegate: ChildVCDelegate? @IBOutlet weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self } } // 实现UITableViewDelegate的滚动回调 extension ChildViewController: UITableViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { // 把偏移量传递给代理(父控制器) delegate?.childVC(self, didScrollWith: scrollView.contentOffset) } }
- 然后在父控制器中,设置自己为子控制器的代理,并实现代理方法:
class ParentViewController: UIViewController, ChildVCDelegate { @IBOutlet weak var targetLabel: UILabel! var childVC1: ChildViewController! var childVC2: ChildViewController! // 可选:跟踪当前显示的子控制器,避免非活跃子VC的滚动干扰 var currentActiveChildVC: ChildViewController? override func viewDidLoad() { super.viewDidLoad() // 初始化并添加子控制器的代码(比如addChild、addSubview等)省略... childVC1.delegate = self childVC2.delegate = self } // 实现代理方法,调整Label的Y坐标 func childVC(_ childVC: ChildViewController, didScrollWith offset: CGPoint) { // 可选:只处理当前显示的子VC的滚动 guard childVC == currentActiveChildVC else { return } // 这里根据需求写你的坐标计算逻辑,比如随滚动偏移联动 // 如果是直接修改frame: targetLabel.frame.origin.y = offset.y * 0.3 + 80 // 示例公式,可自定义 // 如果用AutoLayout,建议修改约束的constant(更规范): // 假设你给Label加了top约束,存为topConstraint属性 // topConstraint.constant = offset.y * 0.3 + 80 } }
方案二:闭包回调(简洁,适合轻量场景)
如果不想写代理协议,闭包是更简洁的选择,代码量更少:
- 在子控制器中定义闭包属性:
class ChildViewController: UIViewController { // 闭包用于传递滚动偏移 var scrollCallback: ((CGPoint) -> Void)? @IBOutlet weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self } } extension ChildViewController: UITableViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { scrollCallback?(scrollView.contentOffset) } }
- 父控制器初始化子VC时,设置闭包:
class ParentViewController: UIViewController { @IBOutlet weak var targetLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() // 初始化第一个子VC let childVC1 = ChildViewController() childVC1.scrollCallback = { [weak self] offset in guard let self = self else { return } // 同样的坐标调整逻辑 self.targetLabel.frame.origin.y = offset.y * 0.3 + 80 } // 添加到父VC的代码省略... // 第二个子VC同理设置scrollCallback let childVC2 = ChildViewController() childVC2.scrollCallback = { [weak self] offset in guard let self = self else { return } self.targetLabel.frame.origin.y = offset.y * 0.3 + 80 } } }
方案三:通知中心(适合跨多层级通信,不推荐父子直接用)
如果你的场景比较特殊,也可以用通知传递,但这种方式耦合度高,一般不推荐父子控制器直接用:
- 子控制器滚动时发送通知:
extension ChildViewController: UITableViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { NotificationCenter.default.post( name: NSNotification.Name("ChildVCDidScroll"), object: scrollView.contentOffset ) } }
- 父控制器监听通知:
class ParentViewController: UIViewController { @IBOutlet weak var targetLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() // 监听通知 NotificationCenter.default.addObserver( self, selector: #selector(handleChildScroll(_:)), name: NSNotification.Name("ChildVCDidScroll"), object: nil ) } @objc private func handleChildScroll(_ notification: Notification) { guard let offset = notification.object as? CGPoint else { return } targetLabel.frame.origin.y = offset.y * 0.3 + 80 } // 记得在销毁时移除监听 deinit { NotificationCenter.default.removeObserver(self) } }
几个注意事项
- 内存泄漏:不管用代理还是闭包,都要注意用
weak引用父控制器,避免循环引用; - AutoLayout优先:如果你的父控制器用AutoLayout布局,不要直接修改Label的frame,建议修改约束的
constant值,这样布局更稳定; - 当前活跃子VC判断:如果两个子VC是切换显示的(比如用TabBar或者PageViewController),记得在父控制器中跟踪当前显示的子VC,只处理它的滚动事件,避免多个子VC同时滚动导致的冲突。
内容的提问来源于stack exchange,提问作者user3138007
相关产品推荐
相关产品推荐

