SwiftUI-UIKit Interop
Bridge UIKit and SwiftUI in both directions: wrap UIKit views and controllers, embed SwiftUI in UIKit screens, and synchronize state without duplicating lifecycle ownership.
See references/representable-recipes.md [blocked] for complete wrapping recipes and references/hosting-migration.md [blocked] for UIKit-to-SwiftUI migration patterns.
Contents
- UIViewRepresentable Protocol
- UIViewControllerRepresentable Protocol
- The Coordinator Pattern
- UIHostingController
- Sizing and Layout
- State Synchronization Patterns
- UIKit Automatic Observation Tracking
- Sendable Considerations
- Common Mistakes
- Review Checklist
- References
UIViewRepresentable Protocol
Use UIViewRepresentable to wrap any UIView subclass for use in SwiftUI.
Required Methods
Lifecycle Timing
Why updateUIView is the most important method: SwiftUI calls it every time any @Binding, @State, @Environment, or @Observable property read by the representable changes. All state synchronization from SwiftUI to UIKit happens here. If you skip a property, the UIKit view will fall out of sync.
Optional: dismantleUIView
Optional: sizeThatFits (iOS 16+)
UIViewControllerRepresentable Protocol
Use UIViewControllerRepresentable to wrap a UIViewController subclass -- typically for system pickers, document scanners, mail compose, or any controller that presents modally.
Handling Results from Presented Controllers
The coordinator captures delegate callbacks and routes results back to SwiftUI through the parent's @Binding or closures:
The Coordinator Pattern
Why Coordinators Exist
UIKit delegates, data sources, and target-action patterns require a reference type (class). SwiftUI representable structs are value types and cannot serve as delegates. The Coordinator is a class instance that SwiftUI creates and manages for you -- it lives as long as the representable view.
Structure
Always nest the Coordinator inside the representable or in an extension. Store a reference to parent (the representable struct) so the coordinator can write back to @Binding properties.
Key Rules
-
Set the delegate in
makeUIView/makeUIViewController, never inupdateUIView. The update method can run many times for state changes affecting the represented view -- setting the delegate there causes redundant assignment and can trigger unexpected side effects. -
Refresh copied parent state yourself. If the coordinator stores the representable in a
var parent, assigncontext.coordinator.parent = selfat the start ofupdateUIVieworupdateUIViewController. Bindings still point at their source of truth, but closures and non-binding values are copied into the coordinator. -
Use
[weak coordinator]in closures to avoid retain cycles between the coordinator and UIKit objects that capture it.
UIHostingController
Embed SwiftUI views inside UIKit view controllers using UIHostingController.
Basic Embedding
The three-step sequence (addChild, add view, didMove) is mandatory. Skipping any step causes containment callbacks to misfire, which breaks appearance transitions and trait propagation.
Sizing Options (iOS 16+)
Updating the Root View
When data changes in UIKit, push new state into the hosted SwiftUI view:
For observable models, pass an @Observable object and SwiftUI tracks changes automatically -- no need to reassign rootView.
UIHostingConfiguration (iOS 16+)
Render SwiftUI content directly inside UICollectionViewCell or UITableViewCell without managing a child hosting controller:
Sizing and Layout
intrinsicContentSize Bridging
UIKit views wrapped in UIViewRepresentable communicate their natural size to SwiftUI through intrinsicContentSize. SwiftUI respects this during layout unless overridden by frame() or fixedSize().
SwiftUI-Owned Geometry
SwiftUI owns the represented view's center, bounds, frame, and transform. Do not set those properties directly on the uiView in makeUIView or updateUIView. Use sizeThatFits, intrinsic content size, SwiftUI layout modifiers, or layout code inside a custom UIKit subview for internal sublayers.
fixedSize() and frame() Interactions
Auto Layout with UIHostingController
When embedding UIHostingController as a child, pin its view with constraints. Use .sizingOptions = [.intrinsicContentSize] so Auto Layout can query the SwiftUI content's natural size for self-sizing cells or variable-height sections.
State Synchronization Patterns
@Binding: Two-Way Sync (SwiftUI <-> UIKit)
Use @Binding when both sides read and write the same value. The coordinator writes to parent.bindingProperty in delegate callbacks; updateUIView reads the binding and pushes it into the UIKit view.
Closures: One-Way Events (UIKit -> SwiftUI)
For fire-and-forget events (button tapped, search submitted, scan completed), pass a closure instead of a binding:
Environment Values
Access SwiftUI environment values inside representable methods via context.environment:
Avoiding Update Loops
updateUIView is called when SwiftUI has new state for the represented view -- including changes triggered by the coordinator writing to a @Binding. Guard against redundant updates to prevent infinite loops:
Without the guard, setting uiView.text may trigger the delegate's textViewDidChange, which writes to parent.text, which triggers updateUIView again.
UIKit Automatic Observation Tracking
For UIKit screens that share an @Observable model with SwiftUI, keep the screen UIKit and read observed state from UIKit's tracked update hooks:
- iOS 26+: use
updateProperties()for labels, colors, visibility, enabled state, and other non-layout UI; use layout hooks for geometry; use cell configuration update handlers for cells. - iOS 18: automatic UIKit tracking requires
UIObservationTrackingEnabledinInfo.plist. - iOS 17:
@Observableexists, but UIKit automatic observation tracking is not available. ManualwithObservationTrackingis one-shot; do not build polling loops around it. - iOS 15-16 or existing
ObservableObject: use CombineobjectWillChange, delegates, notifications, or explicit callbacks.
See references/hosting-migration.md [blocked] for migration patterns.
Sendable Considerations
UIKit delegate protocols are not Sendable. When the coordinator conforms to a UIKit delegate, it inherits main-actor isolation from UIKit. Mark coordinators @MainActor or use nonisolated only for methods that truly do not touch UIKit state. In Swift 6 strict concurrency:
If passing closures across isolation boundaries, ensure they are @Sendable or captured on the correct actor.
Common Mistakes
Review Checklist
- View/controller created in
make*, notupdate* - Coordinator set as delegate in
make*, notupdate* -
@Bindingused for two-way state sync -
updateUIViewhandles all SwiftUI state changes with redundancy guards -
dismantleUIViewcleans up observers/timers if needed - No retain cycles between coordinator and closures (
[weak coordinator]) -
UIHostingControllerproperly added as child (addChild+didMove(toParent:)) - Sizing strategy chosen (
intrinsicContentSizevs fixedframevssizeThatFits) - Represented view geometry left to SwiftUI (
frame,bounds,center,transformnot mutated directly) - Environment values read in
updateUIViewviacontext.environmentwhere needed - UIKit
@Observablereads use automatic tracking hooks on iOS 18+/26+, not polling; iOS 17 is manual one-shot only - Coordinator marked
@MainActorfor strict concurrency - Modal controllers dismiss in all delegate exit paths (success, cancel, error)
-
UIHostingConfigurationused for collection/table view cells instead of manual hosting (iOS 16+)
References
- Wrapping recipes: references/representable-recipes.md [blocked]
- Migration patterns: references/hosting-migration.md [blocked]
- Apple docs: UIViewRepresentable
- Apple docs: UIViewControllerRepresentable
- Apple docs: UIHostingController


