将高度可变的UIHostingController嵌入UIStackView后布局不更新的问题
我在尝试将SwiftUI视图嵌入现有UIKit视图时遇到问题:SwiftUI视图高度可动态变化,但UIStackView无法适配其新尺寸。我创建了一个测试项目来复现该问题。
测试代码
自定义Button代码
class TestButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setupButton() } required init?(coder: NSCoder) { super.init(coder: coder) setupButton() fatalError("init(coder:) has not been implemented") } func setupButton() { setTitleColor(.white, for: .normal) backgroundColor = .red titleLabel?.font = .boldSystemFont(ofSize: 25) layer.cornerRadius = 10 } }
SwiftUI视图代码
struct TestSwiftUIView: View { @State var text: [String] = ["This is a line of text"] var body: some View { VStack { ForEach(text, id: \.self) { text in Text(text) } Button { text.append("New line") } label: { Text("Add line") } .padding() .background(Color.red) } .foregroundColor(.white) .background(Color.green) } }
ViewController代码
class ViewController: UIViewController { var titleLabel = UILabel() var stackView = UIStackView() override func viewDidLoad() { super.viewDidLoad() configureTitleLabels() configureStackView() } func configureStackView() { view.addSubview(stackView) stackView.axis = .vertical stackView.distribution = .fillEqually stackView.spacing = 20 addButtonsToStackView() setStackViewConstraints() let hostingController = UIHostingController(rootView: TestSwiftUIView()) stackView.insertArrangedSubview(hostingController.view, at: 3) } func addButtonsToStackView() { let numberOfButtons = 5 for i in 1...numberOfButtons { let button = TestButton() button.setTitle("\(i)", for: .normal) stackView.addArrangedSubview(button) } } func setStackViewConstraints() { stackView.translatesAutoresizingMaskIntoConstraints = false stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30) .isActive = true stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50).isActive = true stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -50).isActive = true stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30).isActive = true } func configureTitleLabels() { view.addSubview(titleLabel) titleLabel.text = "Test project" titleLabel.font = .systemFont(ofSize: 30) titleLabel.textAlignment = .center titleLabel.numberOfLines = 0 titleLabel.adjustsFontSizeToFitWidth = true setTitleLabelConstaints() } func setTitleLabelConstaints() { titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20) .isActive = true titleLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true titleLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true } }
问题现象
点击SwiftUI视图中的“Add line”按钮后,视图高度会扩展,但UIStackView没有适配这个变化,导致内容被挤压重叠。
已尝试的解决方案(无效)
我曾尝试使用layoutIfNeeded()解决问题,给SwiftUI视图添加了回调:
let callback: () -> Void
修改按钮点击逻辑:
Button { text.append("New line") callback() } label: { Text("Add line") .font(.system(size: 18, weight: .bold, design: nil)) }
在ViewController中调用:
let hostingController = UIHostingController(rootView: TestSwiftUIView(callback: { self.stackView.subviews.forEach { view in view.sizeToFit() view.layoutIfNeeded() } }))
但这种方法没有效果。
如何让UIStackView为高度扩展的SwiftUI视图预留足够空间?
解答
核心问题原因
你的UIStackView设置了distribution = .fillEqually,这个属性会强制所有排列的子视图拥有相同尺寸,无视子视图自身的内容变化。另外,UIHostingController的view默认没有正确将内部SwiftUI视图的动态尺寸约束传递给UIKit布局系统。
解决步骤
修改UIStackView的分布属性
将stackView.distribution = .fillEqually改为stackView.distribution = .fill,或根据需求选择.equalSpacing/.equalCentering,让StackView根据子视图实际尺寸分配空间。给UIHostingController的view添加正确约束
UIHostingController的view需要明确的尺寸约束,让UIKit感知SwiftUI视图的动态变化。设置其宽度匹配StackView,高度自适应内容:
let hostingController = UIHostingController(rootView: TestSwiftUIView()) addChild(hostingController) hostingController.didMove(toParent: self) // 必须调用以保证生命周期正确 stackView.insertArrangedSubview(hostingController.view, at: 3) // 添加约束,让hostingView宽度匹配stackView,高度自适应 hostingController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ hostingController.view.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), hostingController.view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), ])
- 优化SwiftUI视图的布局优先级
给SwiftUI的VStack添加fixedSize(horizontal: false, vertical: true),确保它能正确计算自身动态高度:
struct TestSwiftUIView: View { @State var text: [String] = ["This is a line of text"] var body: some View { VStack { ForEach(text, id: \.self) { text in Text(text) } Button { text.append("New line") } label: { Text("Add line") } .padding() .background(Color.red) } .foregroundColor(.white) .background(Color.green) .fixedSize(horizontal: false, vertical: true) // 添加此行 } }
完整修改后的ViewController关键代码
func configureStackView() { view.addSubview(stackView) stackView.axis = .vertical stackView.distribution = .fill // 修改此处 stackView.spacing = 20 addButtonsToStackView() setStackViewConstraints() let hostingController = UIHostingController(rootView: TestSwiftUIView()) addChild(hostingController) hostingController.didMove(toParent: self) stackView.insertArrangedSubview(hostingController.view, at: 3) hostingController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ hostingController.view.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), hostingController.view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), ]) }
修改后,SwiftUI视图添加新内容时高度会自动更新,UIStackView会重新布局并为其分配足够空间,不会再出现内容挤压问题。
内容的提问来源于stack exchange,提问作者DevB1

