How to use SwiftUI Coordinators to communicate with UIKit
Bridging UIKit delegates and events back into SwiftUI.
09 Feb 2026 · 5 min read
When building SwiftUI apps, we sometimes still need UIKit. Maybe we're integrating an older code base or using system controllers that don't yet have a native SwiftUI equivalent.
In those cases, we can wrap UIKit views or view controllers using UIViewControllerRepresentable. But as soon as delegates or callbacks are involved, we need a way to communicate back to SwiftUI. That's where coordinators come in.
Let's look at a practical example.

The goal
Suppose we want to use Apple's DataScannerViewController to scan QR codes. At the time of writing, DataScannerViewController does not have a native SwiftUI equivalent yet.
From SwiftUI, we want to:
- present the scanner
- scan a code
- pass the scanned value back to SwiftUI state
Wrapping the view controller
To embed a UIKit view controller inside SwiftUI, we conform to UIViewControllerRepresentable:
struct ScannerView: UIViewControllerRepresentable {@Binding var code: String?func makeUIViewController(context: Context) -> DataScannerViewController {let controller = DataScannerViewController(recognizedDataTypes: [.barcode(symbologies: [.qr])])return controller}func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {}}
Now we can present it like any other SwiftUI view:
struct ContentView: View {@State private var code: String?@State private var showingScanner = falsevar body: some View {VStack {Text(code ?? "")Button("Scan QR code") {showingScanner = true}}.sheet(isPresented: $showingScanner) {ScannerView(code: $code)}}}
So far, the code binding we pass into the scanner view never gets updated by it. To be able to do that, we need to setup the scanner view controller's delegate at first.
Why we need a coordinator
UIKit delegates require a reference type (class). But SwiftUI views are structs. So our ScannerView cannot directly act as the delegate.
SwiftUI solves this mismatch by providing coordinators which act as small bridge objects that:
- conform to delegates
- receive UIKit callbacks
- forward data back to SwiftUI
Creating a coordinator
Every representable can define a nested Coordinator type and a makeCoordinator() method:
struct ScannerView: UIViewControllerRepresentable {@Binding var code: String?func makeCoordinator() -> Coordinator {Coordinator(code: $code)}class Coordinator {@Binding var code: String?init(code: Binding<String?>) {_code = code}}}
SwiftUI keeps this coordinator alive for the lifetime of the representable.
Conforming to the delegate
We can then extend the coordinator to serve as DataScannerViewControllerDelegate:
class Coordinator: DataScannerViewControllerDelegate {@Binding var code: String?init(code: Binding<String?>) {_code = code}func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {guard let item = allItems.first else { return }switch item {case .barcode(let code):code = code.payloadStringValuedefault: break}}}
Now the coordinator receives scan results and updates the binding.
Connecting everything
Finally, we assign the delegate inside makeUIViewController:
func makeUIViewController(context: Context) -> DataScannerViewController {let controller = DataScannerViewController(recognizedDataTypes: [.barcode(symbologies: [.qr])])controller.delegate = context.coordinatortry? controller.startScanning()return controller}
context.coordinator gives us the instance SwiftUI created earlier.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



