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

Swift:何时向自定义ContainerView传数据源以正确布局子视图

解决UIPageViewController中自定义ContainerView的数据源传递与动画问题

看起来你碰到了UIPageViewController里自定义容器视图的经典布局和动画问题——我之前做类似项目时也踩过同样的坑,咱们来一步步拆解解决。

问题根源分析

  • viewDidLoad失效的原因:此时自定义容器视图的frame还未完成布局计算,addSegments里用self.frame.width计算子视图宽度会得到错误值,导致子视图布局完全失效。
  • viewDidLayoutSubviews的问题:这个方法会在布局变化时被多次触发(翻页、屏幕旋转、父视图调整都可能触发),重复设置代理会导致addSegments重复执行,动画也会被多次调用;再加上UIPageViewController会预加载相邻页面,动画会在后台提前启动,导致翻页时看不到完整的从头开始的动画。

解决方案:分三步优化

1. 只执行一次初始化布局与代理设置

在storyViewController中添加标记变量,避免viewDidLayoutSubviews重复执行初始化逻辑:

class StoryViewController: UIViewController {
    private var isInitialLayoutDone = false
    private var segmentContainerView: ATCStorySegmentsView!

    override func viewDidLoad() {
        super.viewDidLoad()
        segmentContainerView = ATCStorySegmentsView()
        view.addSubview(segmentContainerView)
        configureSegmentContainerView()
        segmentContainerView.translatesAutoresizingMaskIntoConstraints = false
        // 这里暂不设置代理,等布局完成后再执行
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // 仅在第一次布局完成时执行初始化
        guard !isInitialLayoutDone else { return }
        isInitialLayoutDone = true
        
        // 此时容器视图的frame已经完全计算完成
        segmentContainerView.delegate = self
        // 启动动画
        segmentContainerView.startAnimation()
    }
}

2. 优化容器视图的子视图添加逻辑

确保addSegments仅在frame有效时执行,同时避免重复添加子视图:

class ATCStorySegmentsView: UIView {
    var delegate: ATCSegmentDataSource? {
        didSet {
            // 每次代理变更时,先清空现有子视图(支持数据源更新场景)
            segmentsArray.forEach {
                $0.bottomSegment.removeFromSuperview()
                $0.topSegment.removeFromSuperview()
            }
            segmentsArray.removeAll()
            addSegments()
        }
    }
    private var segmentsArray = [Segment]()
    private let padding: CGFloat = 8 // 假设你的padding是这个值,可根据实际调整

    private func addSegments() {
        guard let segmentQuantity = delegate?.numberOfSegmentsToShow(), segmentQuantity > 0 else { return }
        // 确保当前视图的frame已有效
        guard bounds.width > 0 else {
            // 如果frame还未准备好,延迟到下一轮Runloop再执行
            DispatchQueue.main.async { [weak self] in
                self?.addSegments()
            }
            return
        }
        
        let widthOfSegment = (bounds.width - (padding * CGFloat(segmentQuantity - 1))) / CGFloat(segmentQuantity)
        for i in 0..<segmentQuantity {
            let segment = Segment()
            addSubview(segment.bottomSegment)
            addSubview(segment.topSegment)
            
            let iCGFloat = CGFloat(i)
            let segmentFrame = CGRect(x: iCGFloat * (widthOfSegment + padding), y: 0, width: widthOfSegment, height: bounds.height)
            segment.bottomSegment.frame = segmentFrame
            segment.topSegment.frame = segmentFrame
            segment.topSegment.frame.size.width = 0
            
            segmentsArray.append(segment)
        }
    }
}

3. 解决翻页动画不从头开始的问题

UIPageViewController会预加载相邻页面,导致动画在后台提前启动。我们需要在页面即将显示时重置动画状态:
首先给容器视图添加重置方法:

extension ATCStorySegmentsView {
    func resetAnimation() {
        // 重置所有子视图的动画状态
        segmentsArray.forEach { segment in
            segment.topSegment.frame.size.width = 0
            // 如果有其他动画相关状态(比如计时器、进度),也需要一并重置
        }
        // 停止当前正在运行的动画
        layer.removeAllAnimations()
    }
}

然后在storyViewController的viewWillAppear中触发重置:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    if isInitialLayoutDone {
        segmentContainerView.resetAnimation()
        segmentContainerView.startAnimation()
    }
}

这样调整后,既能保证数据源正确传递、子视图布局准确,又能避免重复执行初始化逻辑,同时翻页时动画会始终从头开始播放。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:34:17