如何使底部UIStackView始终对齐顶部最高UITextView的正下方?
实现底部StackView对齐顶部最高TextView的方案
这个需求我之前做类似的表单布局时刚好碰到过,核心就是让底部的图片StackView精准“贴”在顶部两个TextView中更高的那个的下方对吧?我给你分享两种可行的实现方式,都是基于Auto Layout来搞定的:
方式一:KVO监听TextView内容变化+动态约束调整
这种方式适合TextView内容会动态变化(比如用户输入、网络加载内容)的场景,能实时更新布局:
步骤拆解
基础视图配置:
- 顶部水平StackView包含两个TextView,设置
distribution = .fillEqually让它们宽度均分,同时禁用TextView的滚动(isScrollEnabled = false),这样TextView高度会自动适应内容。 - 底部水平StackView包含两个ImageView,同样设置宽度均分,按需配置图片的contentMode。
- 顶部水平StackView包含两个TextView,设置
初始约束设置:
- 先固定顶部StackView的左右、顶部约束,底部StackView先暂时绑定到顶部StackView的底部(后续会动态调整)。
- 单独保存底部StackView的顶部约束,方便后续修改。
监听TextView高度变化:
- 用KVO监听两个TextView的
contentSize属性,当内容变化导致高度改变时,更新底部StackView的约束。
- 用KVO监听两个TextView的
完整代码示例(Swift)
class CustomView: UIView { // 子视图定义 private let leftTextView = UITextView() private let rightTextView = UITextView() private let textStackView = UIStackView() private let leftImageView = UIImageView() private let rightImageView = UIImageView() private let imageStackView = UIStackView() // 保存顶部约束用于动态更新 private var imageStackTopConstraint: NSLayoutConstraint! override init(frame: CGRect) { super.init(frame: frame) setupViews() setupConstraints() setupTextViewObservers() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() setupConstraints() setupTextViewObservers() } private func setupViews() { // 配置顶部文本StackView textStackView.axis = .horizontal textStackView.distribution = .fillEqually textStackView.spacing = 8 // 让TextView高度自适应内容的关键设置 leftTextView.isScrollEnabled = false rightTextView.isScrollEnabled = false leftTextView.textContainerInset = .zero rightTextView.textContainerInset = .zero leftTextView.textContainer.lineFragmentPadding = 0 rightTextView.textContainer.lineFragmentPadding = 0 textStackView.addArrangedSubview(leftTextView) textStackView.addArrangedSubview(rightTextView) addSubview(textStackView) // 配置底部图片StackView imageStackView.axis = .horizontal imageStackView.distribution = .fillEqually imageStackView.spacing = 8 leftImageView.contentMode = .scaleAspectFit rightImageView.contentMode = .scaleAspectFit imageStackView.addArrangedSubview(leftImageView) imageStackView.addArrangedSubview(rightImageView) addSubview(imageStackView) // 调试用背景色(可删除) leftTextView.backgroundColor = .lightGray rightTextView.backgroundColor = .lightGray leftImageView.backgroundColor = .darkGray rightImageView.backgroundColor = .darkGray } private func setupConstraints() { textStackView.translatesAutoresizingMaskIntoConstraints = false imageStackView.translatesAutoresizingMaskIntoConstraints = false // 固定顶部StackView的位置 NSLayoutConstraint.activate([ textStackView.topAnchor.constraint(equalTo: topAnchor, constant: 16), textStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), textStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16) ]) // 初始底部StackView约束(后续会动态调整) imageStackTopConstraint = imageStackView.topAnchor.constraint(equalTo: textStackView.bottomAnchor) NSLayoutConstraint.activate([ imageStackTopConstraint, imageStackView.leadingAnchor.constraint(equalTo: textStackView.leadingAnchor), imageStackView.trailingAnchor.constraint(equalTo: textStackView.trailingAnchor), imageStackView.heightAnchor.constraint(equalToConstant: 100), // 按需设置图片高度 imageStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -16) ]) // 初始更新一次约束 updateImageStackTopConstraint() } private func setupTextViewObservers() { // 监听TextView内容尺寸变化 leftTextView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil) rightTextView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { guard keyPath == "contentSize" else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } updateImageStackTopConstraint() } private func updateImageStackTopConstraint() { // 先移除旧约束 NSLayoutConstraint.deactivate([imageStackTopConstraint]) // 添加两个低优先级约束(分别对齐两个TextView底部) let leftTopConstraint = imageStackView.topAnchor.constraint(equalTo: leftTextView.bottomAnchor) leftTopConstraint.priority = .defaultHigh // 750 let rightTopConstraint = imageStackView.topAnchor.constraint(equalTo: rightTextView.bottomAnchor) rightTopConstraint.priority = .defaultHigh // 添加两个高优先级约束,确保底部StackView在两个TextView下方 let greaterLeft = imageStackView.topAnchor.constraint(greaterThanOrEqualTo: leftTextView.bottomAnchor) let greaterRight = imageStackView.topAnchor.constraint(greaterThanOrEqualTo: rightTextView.bottomAnchor) // 激活新约束 NSLayoutConstraint.activate([leftTopConstraint, rightTopConstraint, greaterLeft, greaterRight]) // 更新保存的约束引用 imageStackTopConstraint = leftTopConstraint // 强制刷新布局 layoutIfNeeded() } deinit { // 移除观察者避免内存泄漏 leftTextView.removeObserver(self, forKeyPath: "contentSize") rightTextView.removeObserver(self, forKeyPath: "contentSize") } }
方式二:重写layoutSubviews自动更新
如果TextView内容不会频繁变化,或者你不想用KVO,可以重写layoutSubviews方法,每次布局时自动调整底部StackView的位置:
核心修改
把上面的KVO相关代码替换成以下方法:
override func layoutSubviews() { super.layoutSubviews() updateImageStackTopConstraint() }
这样每次视图布局刷新时,都会自动计算并更新底部StackView的顶部约束,实现对齐最高TextView底部的效果。
关键注意点
- 一定要禁用TextView的滚动,否则TextView高度不会自适应内容,始终保持固定高度。
- 如果使用Storyboard/XIB,可以手动添加约束并关联到代码中,再用同样的逻辑动态修改约束优先级或constant。
内容的提问来源于stack exchange,提问作者Jojo Narte
相关产品推荐
相关产品推荐

