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

使用UIViewPropertyAnimator时TableView滚动致ChildVC复位问题求助

iOS子视图控制器展开后滚动TableView复位问题解决

问题描述

主视图控制器(MainVC)中嵌入子视图控制器(MainChildVC),初始状态仅显示子VC顶部区域,拖拽子VC可正常展开,但滚动子VC内的TableView时,子VC会自动复位到初始位置,且无法再次展开。

问题原因

  1. AutoLayout与直接修改Frame冲突:MainVC中为子VC视图添加了AutoLayout约束,但动画时直接修改frame.origin.y,AutoLayout在系统布局更新时会强制重置子VC的frame,导致复位。
  2. 手势识别范围过大:将pan手势添加到了整个子VC视图上,滚动TableView时会触发该pan手势,进而触发子VC的折叠动画。
  3. 状态逻辑与手势代理错误: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
    }
}

关键修改点总结

  1. 用mainChildTopConstraint替代直接修改frame,通过约束变化实现动画,解决AutoLayout重置问题。
  2. 将手势限制在handleArea,避免干扰TableView滚动。
  3. 修复nextState计算逻辑,确保状态切换正确。
  4. 优化手势代理方法,让TableView滚动手势优先响应。

内容的提问来源于stack exchange,提问作者Kenton

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 09:59:59