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:). It is also fine to push or present one when the screen boundary is view-controller-shaped.
UIHostingConfiguration (iOS 16+) is often a better fit for SwiftUI content inside table/collection cells because it avoids creating a full hosting controller per cell.
UIViewRepresentable / UIViewControllerRepresentable wrap UIKit for SwiftUI. makeUIView creates the UIKit object once for that representable identity; updateUIView pushes the latest SwiftUI state into it.
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.
Common bugs: missing child containment calls, no constraints on the hosted view, retain cycles in callbacks, writing to SwiftUI state from updateUIView and causing update loops, or assuming UIKit and SwiftUI have the same identity/lifecycle rules.
Principles
- Use
UIHostingControllerfor screen/controller boundaries; useUIHostingConfigurationfor modern cell content. makeUIViewcreates;updateUIViewsynchronizes. Keep updates idempotent.- Use a
Coordinatorfor delegate-style callbacks and avoid feedback loops when writing bindings.
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)
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
- Cells:
UIHostingConfigurationworks well for SwiftUI row content, but still respect reuse, stable identity, and cheap row bodies. - Navigation: embedding SwiftUI does not automatically unify UIKit
UINavigationControllerstate with SwiftUINavigationStack; choose one owner for navigation decisions.