Question 8
UIKit and SwiftUI interop
How do you embed SwiftUI in UIKit or wrap UIKit for SwiftUI, and what usually goes wrong at the boundary?
Follow-ups
- When would you use
UIHostingControllervsUIHostingConfiguration? - What is a
Coordinatorfor inUIViewRepresentable?
Answer outline
UIHostingController embeds a SwiftUI hierarchy inside UIKit. Treat it like any child view controller: addChild, add its view, constrain it, then call didMove(toParent:). For SwiftUI content inside table and collection cells, UIHostingConfiguration (iOS 16 and later) is the better fit, because it avoids a full hosting controller per cell.
UIViewRepresentable and UIViewControllerRepresentable wrap UIKit for SwiftUI. makeUIView creates the UIKit object once for that representable's identity, and updateUIView pushes the latest SwiftUI state into it.
A Coordinator is the bridge for delegates, data sources, targets, and callbacks. It lets UIKit report changes back into SwiftUI bindings or models without stuffing delegate logic into the view struct.
The usual bugs sit right at the boundary:
- 1.Missing containment:
addChildwithoutdidMove(toParent:), or a hosted view added with no constraints, so appearance callbacks and sizing go wrong. - 2.Retain cycles: closures in the coordinator or callbacks that capture the hosting controller or the wrapped view strongly.
- 3.Update loops: writing to SwiftUI state from
updateUIView, which triggers another update, which writes again. - 4.Mismatched lifecycles: UIKit code that assumes it owns the view's lifetime, when SwiftUI decides when a representable is created, updated, and torn down.
Principles
- Use
UIHostingControllerat screen and view controller boundaries, andUIHostingConfigurationfor cell content. makeUIViewcreates andupdateUIViewsynchronizes, so make updates idempotent: running one twice must change nothing.- Route delegate-style callbacks through a
Coordinator, and guard binding writes so they can't feed back intoupdateUIView.
Standard child containment, with the hosted view pinned to the parent's edges:
let host = UIHostingController(rootView: ProfileView(model: model))
addChild(host)
view.addSubview(host.view)
host.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
host.view.topAnchor.constraint(equalTo: view.topAnchor),
host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
host.didMove(toParent: self)
A UITextField bridged into a SwiftUI binding through a Coordinator, with an equality check so updateUIView doesn't loop:
struct TextFieldWrapper: UIViewRepresentable {
@Binding var text: String
final class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
init(text: Binding<String>) {
self.text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text.wrappedValue = textField.text ?? ""
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
func makeUIView(context: Context) -> UITextField {
let field = UITextField()
field.delegate = context.coordinator
return field
}
func updateUIView(_ field: UITextField, context: Context) {
if field.text != text {
field.text = text
}
}
}
Follow-up angles
UIHostingConfigurationworks well for SwiftUI row content, but reuse, stable identity, and cheap row bodies still apply.- Embedding SwiftUI doesn't unify
UINavigationControllerstate withNavigationStack, so choose one owner for navigation decisions.



