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
@Bindingor@ObservedObjectfor 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 = falsewhen embedding SwiftUI into UIKit with Auto Layout. - Safe area issues: When hosting SwiftUI in UIKit, check
hostingController.viewconstraints and safe area insets. - State not updating: In
UIViewRepresentable, make sure you implementupdateUIViewto apply changes. - Lifecycle confusion: Use
addChild/didMove(toParent:)forUIHostingControllerto 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.