SwiftUI-UIKit Interop
Bridge UIKit and SwiftUI in both directions. Wrap UIKit views and view controllers for use in SwiftUI, embed SwiftUI views inside UIKit screens, and synchronize state across the boundary. Targets iOS 26+ with Swift 6.3 patterns; notes backward-compatible to iOS 16 unless stated otherwise.
See references/representable-recipes.md for complete wrapping recipes and references/hosting-migration.md for UIKit-to-SwiftUI migration patterns.
Contents
UIViewRepresentable Protocol
Use UIViewRepresentable to wrap any UIView subclass for use in SwiftUI.
Required Methods
struct WrappedTextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = .preferredFont(forTextStyle: .body)
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
if uiView.text != text {
uiView.text = text
}
}
}
Lifecycle Timing
| Method |
When Called |
Purpose |
makeCoordinator() |
Before makeUIView. Once per representable lifetime. |
Create the delegate/datasource reference type. |
makeUIView(context:) |
Once, when the representable enters the view tree. |
Allocate and configure the UIKit view. |
updateUIView(_:context:) |
Immediately after makeUIView, then on every relevant state change. |
Push SwiftUI state into the UIKit view. |
dismantleUIView(_:coordinator:) |
When the representable is removed from the view tree. |
Clean up observers, timers, subscriptions. |
sizeThatFits(_:uiView:context:) |
During layout, when SwiftUI needs the view's ideal size. iOS 16+. |
Return a custom size proposal. |
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
static func dismantleUIView(_ uiView: UITextView, coordinator: Coordinator) {
coordinator.cancellables.removeAll()
}
Optional: sizeThatFits (iOS 16+)
@available(iOS 16.0, *)
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: UITextView,
context: Context
) -> CGSize? {
let width = proposal.width ?? UIView.layoutFittingExpandedSize.width
let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
return size
}
UIViewControllerRepresentable Protocol
Use UIViewControllerRepresentable to wrap a UIViewController subclass -- typically for system pickers, document scanners, mail compose, or any controller that presents modally.
struct DocumentScannerView: UIViewControllerRepresentable {
@Binding var scannedImages: [UIImage]
@Environment(\.dismiss) private var dismiss
func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
let scanner = VNDocumentCameraViewController()
scanner.delegate = context.coordinator
return scanner
}
func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) {
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
}
Handling Results from Presented Controllers
The coordinator captures delegate callbacks and routes results back to SwiftUI through the parent's @Binding or closures:
extension DocumentScannerView {
final class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
let parent: DocumentScannerView
init(_ parent: DocumentScannerView) { self.parent = parent }
func documentCameraViewController(
_ controller: VNDocumentCameraViewController,
didFinishWith scan: VNDocumentCameraScan
) {
parent.scannedImages = (0..<scan.pageCount).map { scan.imageOfPage(at: $0) }
parent.dismiss()
}
func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
parent.dismiss()
}
func documentCameraViewController(
_ controller: VNDocumentCameraViewController,
didFailWithError error: Error
) {
parent.dismiss()
}
}
}
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.
struct SearchBarView: UIViewRepresentable {
@Binding var text: String
var onSearch: (String) -> Void
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> UISearchBar {
let bar = UISearchBar()
bar.delegate = context.coordinator
return bar