使用UIViewPropertyAnimator时TableView滚动致ChildVC复位问题求助
iOS子视图控制器展开后滚动TableView复位问题解决
问题描述
主视图控制器(MainVC)中嵌入子视图控制器(MainChildVC),初始状态仅显示子VC顶部区域,拖拽子VC可正常展开,但滚动子VC内的TableView时,子VC会自动复位到初始位置,且无法再次展开。
问题原因
- AutoLayout与直接修改Frame冲突:MainVC中为子VC视图添加了AutoLayout约束,但动画时直接修改
frame.origin.y,AutoLayout在系统布局更新时会强制重置子VC的frame,导致复位。 - 手势识别范围过大:将pan手势添加到了整个子VC视图上,滚动TableView时会触发该pan手势,进而触发子VC的折叠动画。
- 状态逻辑与手势代理错误:
nexnState变量未定义,手势代理方法的位置错误,导致手势判断逻辑失效。
解决方案
1. 改用约束驱动动画
移除直接修改frame的逻辑,改用修改约束的方式实现展开/折叠动画,确保与AutoLayout兼容。
2. 限制手势触发区域
将pan和tap手势仅添加到子VC的handleArea上,避免与TableView的滚动手势冲突。
3. 修复状态管理与手势逻辑
- 定义
mainChildVisible变量并初始化,修正nextState的计算逻辑 - 将手势代理方法移到正确的类中,确保手势识别优先级正确
修改后的代码
MainVC 修改版
enum ChildVCState { case expanded case collapsed } class MainVC: UIViewController, UIGestureRecognizerDelegate { var mainChild: MainChildVC! lazy var mainChildHeight: CGFloat = view.frame.size.height - 50 let mainChildHandleAreaHeight = 80 var runningAnimations = [UIViewPropertyAnimator]() var animationProgressWhenInterrupted: CGFloat = 0 // 新增状态变量,初始为折叠状态 var mainChildVisible = false // 新增子视图顶部约束引用 private var mainChildTopConstraint: NSLayoutConstraint! override func loadView() { super.loadView() view = mainViews } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black self.definesPresentationContext = true setupChild() } private func setupChild() { mainChild = MainChildVC() addChild(mainChild) view.insertSubview(mainChild.view, at: 2) mainChild.didMove(toParent: self) mainChild.view.translatesAutoresizingMaskIntoConstraints = false // 替换高度约束为顶部约束,通过修改top值实现展开/折叠 mainChildTopConstraint = mainChild.view.topAnchor.constraint(equalTo: mainViews.waveAnimationView.bottomAnchor, constant: view.frame.height - view.frame.height * 0.4) NSLayoutConstraint.activate([ mainChildTopConstraint, mainChild.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10), mainChild.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10), mainChild.view.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 1.0) ]) mainChild.view.clipsToBounds = true let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleMainChildTap(recognizer:))) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleMainChildPan(recognizer:))) // 仅在handleArea添加手势 mainChild.mainChildViews.handleArea.addGestureRecognizer(panGestureRecognizer) mainChild.mainChildViews.handleArea.addGestureRecognizer(tapGestureRecognizer) // 设置手势代理 panGestureRecognizer.delegate = self tapGestureRecognizer.delegate = self } // 计算下一个状态 private var nextState: ChildVCState { return mainChildVisible ? .collapsed : .expanded } @objc private func handleMainChildTap(recognizer: UITapGestureRecognizer) { switch recognizer.state { case .ended: animateTransitionIfNeeded(state: nextState, duration: 0.9) default: break } } @objc private func handleMainChildPan(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: startInteractiveTransition(state: nextState, duration: 0.9) case .changed: let translation = recognizer.translation(in: mainChild.mainChildViews.handleArea) var fractionComplete = translation.y / self.mainChildHeight fractionComplete = mainChildVisible ? fractionComplete : -fractionComplete updateInteractiveTransition(fractionCompleted: fractionComplete) case .ended: continueInteractiveTransition() default: break } } private func animateTransitionIfNeeded(state: ChildVCState, duration: TimeInterval) { guard runningAnimations.isEmpty else { return } let constraintAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) { switch state { case .expanded: // 修改顶部约束实现展开 self.mainChildTopConstraint.constant = -10 self.mainChild.mainChildViews.handleArea.backgroundColor = .black case .collapsed: // 修改顶部约束实现折叠 self.mainChildTopConstraint.constant = self.view.frame.height - self.view.frame.height * 0.4 self.mainChild.mainChildViews.handleArea.backgroundColor = .green } self.view.layoutIfNeeded() } constraintAnimator.addCompletion { _ in self.mainChildVisible = !self.mainChildVisible self.runningAnimations.removeAll() } constraintAnimator.startAnimation() runningAnimations.append(constraintAnimator) let cornerRadiusAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear) { switch state { case .expanded: self.mainChild.view.layer.cornerRadius = 12 self.mainChild.mainChildViews.tableView.isUserInteractionEnabled = true case .collapsed: self.mainChild.view.layer.cornerRadius = 0 self.mainChild.mainChildViews.tableView.isUserInteractionEnabled = false } } cornerRadiusAnimator.startAnimation() runningAnimations.append(cornerRadiusAnimator) } private func startInteractiveTransition(state: ChildVCState, duration: TimeInterval) { if runningAnimations.isEmpty { animateTransitionIfNeeded(state: state, duration: duration) } for animator in runningAnimations { animator.pauseAnimation() animationProgressWhenInterrupted = animator.fractionComplete } } private func updateInteractiveTransition(fractionCompleted: CGFloat) { for animator in runningAnimations { animator.fractionComplete = fractionCompleted + animationProgressWhenInterrupted } } private func continueInteractiveTransition() { for animator in runningAnimations { animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) } } // 处理手势冲突,确保TableView滚动优先 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if otherGestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer.view is UITableView { return false } return true } }
MainChildVC 修改说明
无需大幅修改,确保TableView的手势能正常响应即可,补充必要的TableView代理实现:
import UIKit import youtube_ios_player_helper final class MainChildVC: UIViewController, UITableViewDelegate, UITableViewDataSource { let mainChildViews = MainChildViews() // 假设你有这个数据源数组 var videoIDs: [String] = [] override func loadView() { super.loadView() view = mainChildViews } override func viewDidLoad() { super.viewDidLoad() view.alpha = 1 view.backgroundColor = .black _ = Timer.scheduledTimer(timeInterval: 20, target: self, selector: #selector(updateCellData), userInfo: nil, repeats: true) mainChildViews.tableView.delegate = self mainChildViews.tableView.dataSource = self tabBarController?.tabBar.isTranslucent = true tabBarController?.tabBar.backgroundImage = UIImage() tabBarController?.tabBar.shadowImage = UIImage() tabBarController?.tabBar.backgroundColor = UIColor.clear tabBarController?.tabBar.barTintColor = UIColor.clear } @objc private func updateCellData() { if let previousCell = mainChildViews.tableView.cellForRow(at: IndexPath(row: 0, section: 4)) as? ClipsTableViewCell { previousCell.playerView.stopVideo() } guard !videoIDs.isEmpty else { return } videoIDs[0] = videoIDs.randomElement()! mainChildViews.tableView.reloadSections(IndexSet(integer: 4), with: .left) } // UITableView代理实现 func numberOfSections(in tableView: UITableView) -> Int { return 5 // 根据你的UI结构调整 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return section == 4 ? 1 : 0 // 对应你的section 4的行数 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ClipsTableViewCell", for: indexPath) as! ClipsTableViewCell // 配置cell内容,比如设置player的视频ID if indexPath.section == 4 { cell.playerView.load(withVideoId: videoIDs[0]) } return cell } }
关键修改点总结
- 用
mainChildTopConstraint替代直接修改frame,通过约束变化实现动画,解决AutoLayout重置问题。 - 将手势限制在handleArea,避免干扰TableView滚动。
- 修复
nextState计算逻辑,确保状态切换正确。 - 优化手势代理方法,让TableView滚动手势优先响应。
内容的提问来源于stack exchange,提问作者Kenton
相关产品推荐
相关产品推荐

