SwiftQA Random

How do you mix UIKit and SwiftUI safely?

Most production apps mix both today. The safety concerns are specific: state ownership across the bridge, lifecycle mismatches, and performance pitfalls at the boundary.

1. Embedding SwiftUI inside UIKit — UIHostingController

let swiftUIView = ProfileView(viewModel: viewModel)
let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.frame = view.bounds
hostingController.didMove(toParent: self)

Safety concerns: size negotiation with Auto Layout (use sizingOptions/preferredContentSize for dynamic-height content); repeated UIHostingController instantiation in cells is expensive — reuse one instance and update rootView on reuse; safe area behavior can vary by embedding context.

2. Embedding UIKit inside SwiftUI — UIViewRepresentable/UIViewControllerRepresentable

struct LegacyMapView: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }
    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)
    }
    func makeCoordinator() -> Coordinator { Coordinator(self) }
    final class Coordinator: NSObject, MKMapViewDelegate {
        let parent: LegacyMapView
        init(_ parent: LegacyMapView) { self.parent = parent }
    }
}

Safety concerns: updateUIView is called far more often than expected — guard against redundant work; the Coordinator is the correct place for delegate/target-action bridging; there’s no direct SwiftUI-native equivalent to viewWillAppear/viewDidDisappear for wrapped content, so bridge appear/disappear through .onAppear/.onDisappear on the representable.

3. State ownership across the boundary — the most common real bug source

struct ContentView: View {
    @State private var region = MKCoordinateRegion(...)
    var body: some View {
        LegacyMapView(region: $region)  // UIKit reads/writes via binding, SwiftUI is source of truth
    }
}

Decide clearly which side owns the source of truth. If both a UIKit delegate callback and SwiftUI @State can independently mutate the “same” data, you get hard-to-reproduce state-consistency bugs.

4. @MainActor and threading consistency across the boundary

final class Coordinator: NSObject, SomeDelegate {
    func didReceiveUpdate(_ data: Data) {
        Task { @MainActor in
            parent.viewModel.update(data)
        }
    }
}

Not every UIKit delegate callback is guaranteed to run on the main thread — worth checking rather than assuming.

5. Navigation — pick one system as the source of truth, don’t run both simultaneously

SwiftUI’s NavigationStack/NavigationPath and UIKit’s UINavigationController push/pop shouldn’t both try to manage the same navigation stack independently. Most mixed apps pick one system as authoritative per flow.

6. Animation and transition coordination

SwiftUI’s animation system and UIKit’s UIView.animate/UIViewPropertyAnimator don’t automatically coordinate — anything beyond simple crossfades at the boundary often needs deliberate custom transition code.

7. Testing the boundary specifically

Keep the representable/wrapper as thin as possible — just the bridging logic — and push actual business logic into a plain, independently-testable ViewModel/service that both sides depend on.

Interview-ready summary

ConcernMechanism/discipline
SwiftUI inside UIKitUIHostingController, proper child VC containment, watch cell-reuse cost
UIKit inside SwiftUIUIViewRepresentable/UIViewControllerRepresentable, Coordinator for delegate bridging
State ownershipOne clear source of truth per piece of state, not independent mutation from both sides
ThreadingExplicit @MainActor hops for delegate callbacks not guaranteed to be on main thread
NavigationPick one navigation system as authoritative per flow, don’t run both simultaneously
AnimationCustom transition coordination for anything crossing the boundary beyond simple cases
TestabilityKeep the bridging code thin; push logic into a shared, independently-testable layer

One-liner if pressed: “I’d bridge SwiftUI into UIKit via UIHostingController with proper child-view-controller containment, and UIKit into SwiftUI via UIViewRepresentable/UIViewControllerRepresentable with a Coordinator handling delegate callbacks — the real safety discipline is picking one clear owner for any state that crosses the boundary rather than letting both frameworks mutate it independently, being deliberate about which navigation system is authoritative for a given flow, and explicitly hopping to @MainActor in delegate callbacks that aren’t guaranteed to already be on the main thread.”