How to Use SwiftUI and UIKit Together

How to Use SwiftUI and UIKit Together

How to Use SwiftUI and UIKit Together

SwiftUI is the modern way to build interfaces on Apple platforms, but most production apps still rely on UIKit. The good news is that you do not need a full rewrite. Apple designed the two frameworks to interoperate so you can adopt SwiftUI gradually, or embed UIKit where SwiftUI is not enough yet.

This guide shows the practical, supported ways to bridge SwiftUI and UIKit in both directions.

Why mix SwiftUI and UIKit?

  • You can incrementally migrate a large UIKit app without breaking existing screens.
  • You can reuse mature UIKit components (like complex collection views) inside SwiftUI.
  • You can host SwiftUI views inside UIKit to try new UI patterns safely.

Prerequisites

  • Xcode 12+ (preferably the latest version)
  • iOS 13+ for basic SwiftUI hosting (some APIs require newer OS versions)
  • A basic understanding of UIKit view controllers and SwiftUI views

Option 1: Use SwiftUI inside UIKit

The most common approach is to host a SwiftUI view inside a UIKit view controller.

Step 1: Create a SwiftUI view

import SwiftUI

struct ProfileCard: View {
    var body: some View {
        VStack(spacing: 12) {
            Text("Hello, SwiftUI")
                .font(.title)
            Text("This view is hosted inside UIKit.")
                .foregroundColor(.secondary)
        }
        .padding()
    }
}

Step 2: Host it with UIHostingController

import UIKit
import SwiftUI

final class ProfileViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let swiftUIView = ProfileCard()
        let hostingController = UIHostingController(rootView: swiftUIView)

        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        hostingController.didMove(toParent: self)
    }
}

Why this works: UIHostingController is the bridge that renders SwiftUI inside a UIKit container. It behaves like any other view controller.

Option 2: Use UIKit inside SwiftUI

When SwiftUI does not provide a native control (or you already have a UIKit view), you can wrap it.

Step 1: Wrap a UIKit view with UIViewRepresentable

import SwiftUI
import UIKit

struct ActivityIndicator: UIViewRepresentable {
    func makeUIView(context: Context) -> UIActivityIndicatorView {
        let view = UIActivityIndicatorView(style: .medium)
        view.startAnimating()
        return view
    }

    func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
        // Update if needed
    }
}

Step 2: Use it in SwiftUI

struct LoadingView: View {
    var body: some View {
        VStack(spacing: 12) {
            ActivityIndicator()
            Text("Loading...")
        }
    }
}

Why this works: UIViewRepresentable gives SwiftUI a lifecycle to create and update UIKit views safely.

Option 3: Use UIKit view controllers inside SwiftUI

You can wrap a full UIKit view controller instead of just a view.

import SwiftUI
import UIKit

struct LegacyDetailView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        LegacyDetailViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // Update if needed
    }
}

Use it in SwiftUI like any other view:

struct DetailScreen: View {
    var body: some View {
        LegacyDetailView()
    }
}

Data flow between SwiftUI and UIKit

When mixing frameworks, keep data flow simple and explicit:

  • Pass values in initializers when possible.
  • Use @Binding or @ObservedObject for SwiftUI state.
  • For UIKit, expose properties or use delegation.
  • If you need two-way updates, use a shared observable model class that both sides can read and write.

Common issues and fixes

  • Layout glitches: Ensure you set translatesAutoresizingMaskIntoConstraints = false when embedding SwiftUI into UIKit with Auto Layout.
  • Safe area issues: When hosting SwiftUI in UIKit, check hostingController.view constraints and safe area insets.
  • State not updating: In UIViewRepresentable, make sure you implement updateUIView to apply changes.
  • Lifecycle confusion: Use addChild / didMove(toParent:) for UIHostingController to keep UIKit lifecycle correct.

Best practices

  • Start by migrating low-risk screens to SwiftUI.
  • Wrap existing UIKit components instead of rewriting everything at once.
  • Keep view models and business logic framework-agnostic.
  • Use SwiftUI previews to iterate faster, even if the screen is still hosted in UIKit.

Conclusion

You can mix SwiftUI and UIKit safely and effectively. Hosting SwiftUI inside UIKit is the easiest entry point, while UIViewRepresentable and UIViewControllerRepresentable let you pull UIKit into SwiftUI when you need to. This incremental approach lets you modernize your UI without a full rewrite.

If you want, I can also provide a migration checklist or patterns for large apps.