Swift自定义UIView子视图布局异常问题求助
嘿,我一眼就看到你这里的问题根源了——你的容器视图还没完成AutoLayout布局,就急着用它的frame计算子视图尺寸,再加上translatesAutoresizingMaskIntoConstraints的设置顺序错了,才导致分段显示异常。下面给你拆解原因和具体的修复办法:
为啥会出问题?
约束转译的时机错了:
你先调用configureSegmentContainerView加了AutoLayout约束,之后才设置segmentContainerView.translatesAutoresizingMaskIntoConstraints = false。这相当于告诉系统:“先给我用自动生成的约束布局,然后再关掉自动转译”——结果就是你手动加的约束根本没生效,容器的frame还是系统默认的错误尺寸,后续算分段宽度自然就错了。layoutIfNeeded()白调用了:
在addSegments里调用这俩方法的时候,容器的约束还没生效,self.frame.width根本不是AutoLayout计算后的最终宽度,用这个错误的宽度算出来的分段尺寸肯定不对,才会出现只显示3个还超出容器的情况。
修复方案(两种可选)
方案一:彻底改用AutoLayout布局子视图(推荐)
既然容器用了AutoLayout,子视图也跟着用,彻底摆脱frame的依赖,以后再也不会有尺寸计算错误的问题。
首先修正viewDidLoad里的代码顺序:
override func viewDidLoad() { super.viewDidLoad() setupDataSource() segmentContainerView = ATCStorySegmentsView() view.addSubview(segmentContainerView) // 先关自动转译,再加约束!这步必须在前 segmentContainerView.translatesAutoresizingMaskIntoConstraints = false configureSegmentContainerView() segmentContainerView.numberOfSegments = friendStory.count }
然后重写addSegments和布局逻辑,用UIStackView来自动管理分段的宽度和间距:
private func addSegments() { guard let numberOfSegments = numberOfSegments else { return } // 先清理旧的分段,避免重复添加 segmentsArray.forEach { $0.bottomSegment.removeFromSuperview() $0.topSegment.removeFromSuperview() } segmentsArray.removeAll() // 创建水平栈视图,自动平分宽度、管理间距 let stackView = UIStackView() stackView.axis = .horizontal stackView.spacing = padding // 这里用你原来定义的padding值 stackView.distribution = .fillEqually stackView.translatesAutoresizingMaskIntoConstraints = false addSubview(stackView) // 把栈视图钉死在容器的四个边缘 NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: self.topAnchor), stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor), stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor) ]) for _ in 0..<numberOfSegments { let segment = Segment() // 让分段的高度和容器一致 NSLayoutConstraint.activate([ segment.bottomSegment.heightAnchor.constraint(equalTo: stackView.heightAnchor), segment.topSegment.heightAnchor.constraint(equalTo: stackView.heightAnchor) ]) // bottomSegment加入栈视图,自动分配宽度 stackView.addArrangedSubview(segment.bottomSegment) // topSegment要覆盖在bottomSegment上,所以直接加在容器上 addSubview(segment.topSegment) // 让topSegment和bottomSegment完全重合,初始宽度设为0 NSLayoutConstraint.activate([ segment.topSegment.topAnchor.constraint(equalTo: segment.bottomSegment.topAnchor), segment.topSegment.bottomAnchor.constraint(equalTo: segment.bottomSegment.bottomAnchor), segment.topSegment.leadingAnchor.constraint(equalTo: segment.bottomSegment.leadingAnchor), segment.topSegment.widthAnchor.constraint(equalToConstant: 0) ]) segmentsArray.append(segment) } }
这个方案的好处是完全不用手动计算尺寸,StackView会自动处理所有布局适配,不管容器尺寸怎么变,分段都会自动调整。
方案二:坚持用frame布局的兼容修复
如果你不想改AutoLayout,那就要确保容器的frame已经正确计算后再生成子视图:
- 修正
viewDidLoad的顺序:
override func viewDidLoad() { super.viewDidLoad() setupDataSource() segmentContainerView = ATCStorySegmentsView() view.addSubview(segmentContainerView) segmentContainerView.translatesAutoresizingMaskIntoConstraints = false configureSegmentContainerView() // 这里先不赋值numberOfSegments,等布局完成后再赋值 }
- 把
numberOfSegments的赋值移到viewDidLayoutSubviews里,这个方法会在视图布局完成后调用:
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 只赋值一次,避免重复调用addSegments if segmentContainerView.numberOfSegments == nil { segmentContainerView.numberOfSegments = friendStory.count } }
- 修改
addSegments,去掉无效的布局调用,用bounds代替frame(bounds是视图内部的真实尺寸):
private func addSegments() { guard let numberOfSegments = numberOfSegments else { return } // 清理旧分段 segmentsArray.forEach { $0.bottomSegment.removeFromSuperview() $0.topSegment.removeFromSuperview() } segmentsArray.removeAll() // 现在self.bounds.width是AutoLayout计算后的正确宽度 let widthOfSegment : CGFloat = (self.bounds.width - (padding * CGFloat(numberOfSegments - 1))) / CGFloat(numberOfSegments) for i in 0..<numberOfSegments { let segment = Segment() addSubview(segment.bottomSegment) addSubview(segment.topSegment) let iCG = CGFloat(i) let segmentFrame = CGRect(x: iCG * (widthOfSegment + padding), y: 0, width: widthOfSegment, height: self.bounds.height) segment.bottomSegment.frame = segmentFrame segment.topSegment.frame = segmentFrame segment.topSegment.frame.size.width = 0 segmentsArray.append(segment) } }
验证效果
不管用哪种方案,现在容器都会先完成AutoLayout布局,得到正确的宽度后再生成分段,4个分段就能正常显示,不会超出容器范围了。
内容的提问来源于stack exchange,提问作者Osama Naeem

