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 :(
You're running into a couple problems... actually, several :(
TL;DR version:
UIHostingController views don't work in the same way as UIKit views.fillEqually, which means the arranged subviews will never change size.The long version:
First, when UIKit loads a UIHostingController view, it uses that view's .intrinsicContentSize. But, because of the way SwiftUI views work, that doesn't "automatically" update.
You can see this by stripping your UIKit view controller down to only the hosting controller:
class SimpleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let hostingController = UIHostingController(rootView: TestSwiftUIView())
// let's unwrap the view to save a little typing
guard let v = hostingController.view else { return }
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
// now let's add that view leading/trailing with 40-points on each side
// and vertically centered (no Height constraint)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// let's give the view a background color
// so we can see its frame
v.backgroundColor = .systemYellow
}
}
This is what we get:



As we see, the green vStack expands vertically, but the yellow view doesn't change.
There are a couple ways to "fix" that...
First, you were on the right track with your callback approach, but let's do it like this.
We'll add a var property to hold a reference to the hosted view:
// so we have a reference to the hosting controller's view
var hcView: UIView!
and when we get the callback, we'll update it like this:
func updateSize() {
hcView.setNeedsLayout()
hcView.layoutIfNeeded()
hcView.invalidateIntrinsicContentSize()
}
Here's a modified example:
struct CallbackTestSwiftUIView: View {
let callback: () -> Void
@State var strings: [String] = [
"This is a line of text",
]
var body: some View {
VStack {
ForEach(strings, id: \.self) { text in
Text(text)
}
Button {
strings.append("New line \(strings.count)")
callback()
} label: {
Text("Add line")
}
.padding()
.background(Color.red)
}
.foregroundColor(.white)
.background(Color.green)
}
}
class CallbackSimpleViewController: UIViewController {
// so we have a reference to the hosting controller's view
var hcView: UIView!
func updateSize() {
hcView.setNeedsLayout()
hcView.layoutIfNeeded()
hcView.invalidateIntrinsicContentSize()
}
override func viewDidLoad() {
super.viewDidLoad()
let hostingController = UIHostingController(rootView: CallbackTestSwiftUIView(callback: {
self.updateSize()
}))
// let's unwrap the view to save a little typing
guard let v = hostingController.view else { return }
// save the view reference
self.hcView = v
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
// now let's add that view leading/trailing with 40-points on each side
// and vertically centered (no Height constraint)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// let's give the view a background color
// so we can see its frame
v.backgroundColor = .systemYellow
}
}
and here's the output, with the yellow view growing along with the green view:



So -- woo hoo! Just implement that with your stack view layout and we're done, right?
Whoops, not quite.
You gave your stack view Top and Bottom constraints, and set its .distribution = .fillEqually.
So, the stack view says "I have 6 arranged subviews now, so make them all the same heights and never let the heights change!!!
You might try .distribution = .fillProportionally ... unfortunately, that fails miserably when the stack view's spacing is not Zero. There are ways around that, but it might not give you the desired result anyway.
So, let's use the CallbackTestSwiftUIView and a slightly modified version of your original ViewController with the stack view and buttons:
class CallbackInStackViewController: UIViewController {
var titleLabel = UILabel()
var stackView = UIStackView()
// so we have a reference to the hosting controller's view
var hcView: UIView!
func updateSize() {
hcView.setNeedsLayout()
hcView.layoutIfNeeded()
hcView.invalidateIntrinsicContentSize()
}
override func viewDidLoad() {
super.viewDidLoad()
configureTitleLabels()
configureStackView()
}
func configureStackView() {
view.addSubview(stackView)
stackView.axis = .vertical
// use .fill instead of .fillEqually
//stackView.distribution = .fillEqually
stackView.distribution = .fill
stackView.spacing = 20
addButtonsToStackView()
setStackViewConstraints()
let hostingController = UIHostingController(rootView: CallbackTestSwiftUIView(callback: {
self.updateSize()
}))
// let's unwrap the view to save a little typing
guard let v = hostingController.view else { return }
// save the view reference
self.hcView = v
stackView.insertArrangedSubview(v, at: 3)
// let's give the view a background color
// so we can see its frame
v.backgroundColor = .systemYellow
}
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
// remove stack view's bottom anchor
//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
// don't let title label stretch vertically
titleLabel.setContentHuggingPriority(.required, for: .vertical)
}
}
Which may or may not give you what you need:



but at least we have the sizing in the stack view working.
Some searching comes up with various ways to make "auto-sizing" hosting controllers ... I do very little with SwiftUI, so I can't give you a recommendation on which approach might be the best.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With