主StackView嵌套ScrollView内StackView时TextView高度约束设置问题
解决TextView自动收缩+占满可用空间的布局问题
我来帮你搞定这个自动布局的需求!你的核心目标是让descriptionTextView在页面有足够空间时占满主StackView的中间区域,当添加评论后自动收缩到合适高度,同时整个内容区域可以滚动。咱们一步步调整现有代码:
需要修改的关键点:
- 移除固定高度约束:你现在给
descriptionTextView加了固定400的高度,这会阻止它收缩,得先删掉这个约束。 - 添加优先级较低的默认高度:给TextView加一个默认高度400,但优先级设为低于
required(比如750),这样当评论占满空间时,这个约束会被自动打破,TextView会收缩到内容所需的最小高度。 - 修复ScrollView的宽度约束:你的
contentStackView只绑定了ScrollView的leading/trailing,但没有设置宽度约束,这会导致ScrollView的contentSize宽度异常,需要让contentStackView的宽度等于contentScrollView的宽度(或者直接绑定到mainStackView的宽度,因为mainStackView已经和safeArea对齐了)。 - 清理冗余约束:
descriptionTextView已经在contentStackView里,而contentStackView的alignment是.fill,所以不需要再给TextView加leading/trailing约束,删掉这些冗余的。 - 确保Footer的高度稳定:给
footerStackView加一个基于intrinsicContentSize的高度约束,避免主StackView布局混乱。
修改后的完整代码:
import UIKit class DetailSampleViewController: UIViewController { lazy var mainStackView: UIStackView = { let m = UIStackView() m.axis = .vertical m.alignment = .fill m.distribution = .fill m.spacing = 10 m.translatesAutoresizingMaskIntoConstraints = false m.addArrangedSubview(titleTextField) m.addArrangedSubview(contentScrollView) m.addArrangedSubview(footerStackView) return m }() lazy var titleTextField: UITextField = { let t = UITextField() t.borderStyle = .roundedRect t.placeholder = "Some Fancy Placeholder" t.text = "Some Fancy Title" t.translatesAutoresizingMaskIntoConstraints = false return t }() lazy var contentScrollView: UIScrollView = { let s = UIScrollView() s.contentMode = .scaleToFill s.keyboardDismissMode = .onDrag s.translatesAutoresizingMaskIntoConstraints = false s.addSubview(contentStackView) return s }() lazy var contentStackView: UIStackView = { let s = UIStackView() s.translatesAutoresizingMaskIntoConstraints = false s.axis = .vertical s.alignment = .fill // 把distribution从equalSpacing改成fill,让TextView优先占满空间 s.distribution = .fill s.spacing = 10 s.contentMode = .scaleToFill s.addArrangedSubview(descriptionTextView) // 先加几条测试评论,你可以动态添加 s.addArrangedSubview(getCommentLabel(with: "Some fancy comment")) s.addArrangedSubview(getCommentLabel(with: "Another fancy comment")) return s }() lazy var descriptionTextView: UITextView = { let tv = UITextView() tv.font = UIFont.systemFont(ofSize: 17.0) tv.clipsToBounds = true tv.layer.cornerRadius = 5.0 tv.layer.borderWidth = 0.25 tv.translatesAutoresizingMaskIntoConstraints = false tv.text = """ Some fancy textfield text, spanning over multiple lines ... """ // 禁用TextView的滚动,因为我们外层已经有ScrollView了 tv.isScrollEnabled = false return tv }() lazy var footerStackView: UIStackView = { let f = UIStackView() f.axis = .horizontal f.alignment = .fill f.distribution = .fillEqually f.translatesAutoresizingMaskIntoConstraints = false // 加上这个,方便加约束 let commentLabel = UILabel() commentLabel.text = "Comments" let addCommentButton = UIButton(type: UIButton.ButtonType.system) addCommentButton.setTitle("Add Comment", for: .normal) // 给按钮添加点击事件,动态添加评论 addCommentButton.addTarget(self, action: #selector(addCommentTapped), for: .touchUpInside) f.addArrangedSubview(commentLabel) f.addArrangedSubview(addCommentButton) return f }() override func loadView() { view = UIView() view.backgroundColor = .systemBackground navigationController?.isToolbarHidden = true view.addSubview(mainStackView) NSLayoutConstraint.activate([ mainStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12), mainStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12), mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12), mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12), titleTextField.heightAnchor.constraint(equalToConstant: titleTextField.intrinsicContentSize.height), // 给contentScrollView设置高度约束:主StackView的高度减去title和footer的高度 contentScrollView.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 10), contentScrollView.bottomAnchor.constraint(equalTo: footerStackView.topAnchor, constant: -10), contentScrollView.leadingAnchor.constraint(equalTo: mainStackView.leadingAnchor), contentScrollView.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor), // 修复contentStackView的约束:宽度等于contentScrollView的宽度,同时绑定四边 contentStackView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor), contentStackView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor), contentStackView.topAnchor.constraint(equalTo: contentScrollView.topAnchor), contentStackView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor), contentStackView.widthAnchor.constraint(equalTo: contentScrollView.widthAnchor), // 给TextView加一个优先级较低的默认高度,空间足够时保持400,不够时收缩 descriptionTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), // 最小高度,可选 descriptionTextView.heightAnchor.constraint(equalToConstant: 400).withPriority(.defaultHigh), // 给footerStackView加高度约束,基于intrinsicContentSize footerStackView.heightAnchor.constraint(equalToConstant: footerStackView.intrinsicContentSize.height) ]) } override func viewDidLoad() { super.viewDidLoad() title = "Detail View" } // 动态添加评论的方法 @objc func addCommentTapped() { let newComment = getCommentLabel(with: "New comment added!") contentStackView.addArrangedSubview(newComment) // 触发布局更新 UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() } } func getCommentLabel(with text: String) -> UILabel { let l = UILabel() l.layer.borderWidth = 0.25 l.layer.cornerRadius = 5.0 // 加个圆角,和TextView统一风格 l.translatesAutoresizingMaskIntoConstraints = false l.text = text l.numberOfLines = 0 // 允许评论换行 return l } }
关键细节解释:
- TextView的滚动禁用:因为外层已经有
contentScrollView了,TextView自己的滚动会导致冲突,所以设tv.isScrollEnabled = false,让它的高度由内容决定。 - 优先级约束:
withPriority(.defaultHigh)(对应750)比默认的required(1000)低,当评论占用空间导致TextView无法保持400高度时,这个约束会被自动忽略,TextView会收缩到内容高度。 - 动态添加评论:给按钮加了点击事件,每次点击添加新评论,调用
layoutIfNeeded()触发动画更新布局。 - contentStackView宽度约束:确保ScrollView的contentSize宽度正确,不会出现横向滚动(除非你需要)。
这样修改后,当页面空间足够时,TextView会保持400高度;当添加的评论越来越多,主StackView的中间区域空间不足时,TextView会自动收缩,同时整个contentScrollView可以滚动查看所有内容。
内容的提问来源于stack exchange,提问作者appfrosch
相关产品推荐
相关产品推荐

