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
相关产品推荐
相关产品推荐

