I have an issue where I'm trying to insert a SwiftUI view into an existing UIKit view. The SwiftUI view can change height dynamically, but the UIStackView does not adjust to the new size. I've created a (hideous) test project to highlight this.
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 View:
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
}
}
Here is the result:
How can we ensure that the UIStackView allows enough space for the expanding SwiftUI view?
UPDATE
I had an idea that I may need to do something with layoutIfNeeded()
. So I added a callback to the button within the SwiftUI View as follows:
let callback: () -> Void
And then in the button function:
Button {
text.append("New line")
callback()
} label: {
Text("Add line")
.font(.system(size: 18, weight: .bold, design: nil))
}
Then in ViewController:
let hostingController = UIHostingController(rootView: TestSwiftUIView(callback: {
self.stackView.subviews.forEach { view in
view.sizeToFit()
view.layoutIfNeeded()
}
}))
Sadly this had no impact :(