Two pitfalls to avoid when working with UIHostingController

Volodymyr Dudchak
Arcush Tech
Published in
4 min readJan 8, 2024

SwiftUI is really great and is definitely the future of UI development on iOS. But as I mentioned in my previous article, it is not always possible to achieve everything you want using SwiftUI alone. Thankfully, Apple gives us tools to easily mix SwiftUI and UIKit approaches. In this article, I’m going to warn you about two possible issues you may encounter when embedding SwiftUI views in your existing UIKit code using UIHostingController so that you can save hours I’ve spent debugging those issues while developing my indie app Arcush.

1. Embedded SwiftUI view doesn’t behave properly without preserving UIHostingController

When you want to use a SwiftUI view as a part of a UIKit view, it may be tempting to embed the former inside UIHostingController and then simply plug out the root view of the controller, ignoring UIHostingController completely. Even though it might seem to work just fine in most cases, there are scenarios where it can hit you hard. Take a look at the following silly example:

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let contentViewController = UIHostingController(
rootView: ContentView()
)
contentViewController.view
.translatesAutoresizingMaskIntoConstraints = false
contentViewController.view
.backgroundColor = .blue.withAlphaComponent(0.1)
view.addSubview(contentViewController.view)
NSLayoutConstraint.activate([
contentViewController.view
.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
contentViewController.view
.centerYAnchor
.constraint(equalTo: view.centerYAnchor)
])

}
}

struct ContentView: View {
@State var isButtonShown: Bool = false

var body: some View {
VStack(spacing: 16) {
if isButtonShown {
Button("Button") {
print("Tapped")
}
}
Toggle("Show button", isOn: $isButtonShown)
}
}
}
The embedded SwiftUI frame does not change. The background indicates the frame of the UIHostingController view.

As you might see, the initial frame of the SwiftUI view is limited by Toggle, since the button is hidden. But as soon as we switch the toggle and the button appears, the frame is not getting recalculated. The only way to make it work is to set the sizingOption of the hosting controller so that it will use the intrinsic size of the embedded view:

class ViewController: UIViewController {    
override func viewDidLoad() {
super.viewDidLoad()
let contentViewController = UIHostingController(
rootView: ContentView()
)
addChild(contentViewController)
contentViewController.view
.translatesAutoresizingMaskIntoConstraints = false
contentViewController.view
.backgroundColor = .blue.withAlphaComponent(0.1)
view.addSubview(contentViewController.view)
contentViewController.didMove(toParent: self)
NSLayoutConstraint.activate([
contentViewController.view
.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
contentViewController.view
.centerYAnchor
.constraint(equalTo: view.centerYAnchor)
])

contentViewController.sizingOptions = .intrinsicContentSize
}
}
The layout works properly with the intrinsic size of the embedded SwiftUI view now.

I’m not sure if there are other side effects when the hosting controller is not preserved, but my advice is to always store it somewhere so that its lifetime matches the lifetime of the view it embeds. The best way to achieve it is to add UIHostingController as a child view controller, or at least save it in the property if you create it inside another UIView where you don’t have access to the parent view controller. I hope that one day Apple will make _UIHostingView public, which already exists and which is what UIHostingController uses to host the SwiftUI view, but until that day, UIHostingController is the best tool we have for the interoperability.

2. UIHostingController always takes safe area insets into account

Let’s try the same sample code as above, but instead of constraining the view to the center of the container, we will constrain it to the bottom of the screen, ignoring the safe area insets. I’ve also added a view with the red background to indicate the bottom safe inset:

class ViewController: UIViewController {    
override func viewDidLoad() {
super.viewDidLoad()
let safeAreaView = UIView()
safeAreaView.translatesAutoresizingMaskIntoConstraints = false
safeAreaView.backgroundColor = .red.withAlphaComponent(0.1)
view.addSubview(safeAreaView)
NSLayoutConstraint.activate([
safeAreaView
.bottomAnchor
.constraint(equalTo: view.bottomAnchor),
safeAreaView
.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
safeAreaView
.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
safeAreaView
.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])

let contentViewController = UIHostingController(
rootView: ContentView()
)
addChild(contentViewController)
contentViewController.view
.translatesAutoresizingMaskIntoConstraints = false
contentViewController.view
.backgroundColor = .blue.withAlphaComponent(0.1)
view.addSubview(contentViewController.view)
contentViewController.didMove(toParent: self)
NSLayoutConstraint.activate([
contentViewController.view
.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
contentViewController.view
.bottomAnchor
.constraint(equalTo: view.bottomAnchor)
])

contentViewController.sizingOptions = .intrinsicContentSize
}
}
The hosting controller automatically adjusts its content to the safe area.

As you can see, the bottom inset is automatically applied to the UIHostingController content to consider the safe area. It is also not possible to ignore it using the .ignoreSafeArea() modifier inside the SwiftUI view. The only option to turn it off is to use the safeAreaRegions property of UIHostingController, which is available starting from the iOS 16.4:

        contentViewController.safeAreaRegions = []

If you need to support this behavior on older iOS versions, you would need to implement some ugly hacks, like the one described in this post.

I hope this article is helpful and you will avoid the bugs I’ve spent some time to fix and understand. Please follow this blog to get more tips in the future. And if you are interested in productivity or want to learn more about the app I’m working on, visit our non-technical blog.

--

--

Volodymyr Dudchak
Arcush Tech

I'm a passionate iOS / macOS developer. Ex-Lyft, ex-Macpaw, currently developing the daily planner app: https://arcush.com