Loading...
Loading...
Bridge UIKit and SwiftUI — wrap UIKit views/view controllers in SwiftUI using UIViewRepresentable/UIViewControllerRepresentable, embed SwiftUI in UIKit with UIHostingController, and handle the Coordinator delegate pattern. Use when integrating camera previews, map views, web views, mail compose, document scanners, PDF renderers, text views with attributed text, or any third-party UIKit SDK into a SwiftUI app. Also use when migrating a UIKit app to SwiftUI incrementally, or when needing UIKit features not yet available in native SwiftUI.
npx skill4agent add dpearson2699/swift-ios-skills swiftui-uikit-interopreferences/representable-recipes.mdreferences/hosting-migration.mdUIViewRepresentableUIViewstruct WrappedTextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
// Called ONCE when SwiftUI inserts this view into the hierarchy.
// Create and return the UIKit view. One-time setup goes here.
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = .preferredFont(forTextStyle: .body)
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
// Called on EVERY SwiftUI state change that affects this view.
// Synchronize SwiftUI state into the UIKit view.
// Guard against redundant updates to avoid loops.
if uiView.text != text {
uiView.text = text
}
}
}| Method | When Called | Purpose |
|---|---|---|
| Before | Create the delegate/datasource reference type. |
| Once, when the representable enters the view tree. | Allocate and configure the UIKit view. |
| Immediately after | Push SwiftUI state into the UIKit view. |
| When the representable is removed from the view tree. | Clean up observers, timers, subscriptions. |
| During layout, when SwiftUI needs the view's ideal size. iOS 16+. | Return a custom size proposal. |
updateUIView@Binding@State@Environment@Observablestatic func dismantleUIView(_ uiView: UITextView, coordinator: Coordinator) {
// Remove observers, invalidate timers, cancel subscriptions.
// The coordinator is passed in so you can access state stored on it.
coordinator.cancellables.removeAll()
}@available(iOS 16.0, *)
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: UITextView,
context: Context
) -> CGSize? {
// Return nil to fall back to UIKit's intrinsicContentSize.
// Return a CGSize to override SwiftUI's sizing for this view.
let width = proposal.width ?? UIView.layoutFittingExpandedSize.width
let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
return size
}UIViewControllerRepresentableUIViewControllerstruct 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) {
// Usually empty for modal controllers -- nothing to push from SwiftUI.
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
}@Bindingextension 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()
}
}
}classclassparent@Bindingstruct 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 // Set delegate HERE, not in updateUIView
return bar
}
func updateUIView(_ uiView: UISearchBar, context: Context) {
if uiView.text != text {
uiView.text = text
}
}
final class Coordinator: NSObject, UISearchBarDelegate {
var parent: SearchBarView
init(_ parent: SearchBarView) { self.parent = parent }
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
parent.text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
parent.onSearch(parent.text)
searchBar.resignFirstResponder()
}
}
}makeUIViewmakeUIViewControllerupdateUIViewparentupdateUIView@Bindingparent[weak coordinator]UIHostingControllerfinal class ProfileViewController: UIViewController {
private let hostingController = UIHostingController(rootView: ProfileView())
override func viewDidLoad() {
super.viewDidLoad()
// 1. Add as child
addChild(hostingController)
// 2. Add and constrain the view
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
// 3. Notify the child
hostingController.didMove(toParent: self)
}
}@available(iOS 16.0, *)
hostingController.sizingOptions = [.intrinsicContentSize]| Option | Effect |
|---|---|
| The hosting controller's view reports its SwiftUI content size as |
| Updates |
func updateProfile(_ profile: Profile) {
hostingController.rootView = ProfileView(profile: profile)
}@ObservablerootViewUICollectionViewCellUITableViewCell@available(iOS 16.0, *)
func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.contentConfiguration = UIHostingConfiguration {
ItemRow(item: items[indexPath.item])
}
return cell
}UIViewRepresentableintrinsicContentSizeframe()fixedSize()| SwiftUI Modifier | Effect on Representable |
|---|---|
| No modifier | SwiftUI uses |
| Forces the representable to its ideal (intrinsic) size in both axes. |
| Fixes width to intrinsic; height remains flexible. |
| Overrides the proposed size; UIKit view receives this size. |
UIHostingController.sizingOptions = [.intrinsicContentSize]@Bindingparent.bindingPropertyupdateUIView// SwiftUI -> UIKit: in updateUIView
if uiView.text != text { uiView.text = text }
// UIKit -> SwiftUI: in Coordinator delegate method
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}struct WebViewWrapper: UIViewRepresentable {
let url: URL
var onNavigationFinished: ((URL) -> Void)?
}context.environmentfunc updateUIView(_ uiView: UITextView, context: Context) {
let isEnabled = context.environment.isEnabled
uiView.isEditable = isEnabled
// Respond to color scheme changes
let colorScheme = context.environment.colorScheme
uiView.backgroundColor = colorScheme == .dark ? .systemGray6 : .white
}updateUIView@Bindingfunc updateUIView(_ uiView: UITextView, context: Context) {
// GUARD: Only update if values actually differ
if uiView.text != text {
uiView.text = text
}
}uiView.texttextViewDidChangeparent.textupdateUIViewSendable@MainActornonisolated@MainActor
final class Coordinator: NSObject, UISearchBarDelegate {
var parent: SearchBarView
init(_ parent: SearchBarView) { self.parent = parent }
// Delegate methods are main-actor-isolated -- safe to access UIKit and @Binding.
}@SendableupdateUIViewmakeUIViewupdateUIViewupdateUIViewupdateUIViewmakeUIViewmakeUIViewControllerWKWebViewMKMapView[weak coordinator]parent.dismiss()parent.dismiss().sheetdismantleUIViewNotificationCenterCombineTimerdismantleUIViewUIHostingControllersizingOptionsframe@StateViewparent@Binding@StateViewaddChilddidMove(toParent:)UIHostingControlleraddChild(_:)didMove(toParent:)make*update*make*update*@BindingupdateUIViewdismantleUIView[weak coordinator]UIHostingControlleraddChilddidMove(toParent:)intrinsicContentSizeframesizeThatFitsupdateUIViewcontext.environment@MainActorUIHostingConfigurationsearchAppleDocumentationfetchAppleDocumentation/documentation/SwiftUI/UIViewRepresentable/documentation/SwiftUI/UIHostingControllerreferences/representable-recipes.mdreferences/hosting-migration.md