iosNudge

How do you use UIKit in SwiftUI and SwiftUI in UIKit?

MediumAsked oftenSwiftUIUIKit 2 min read

TL;DR

  • UIKit inside SwiftUI: UIViewRepresentable for views, UIViewControllerRepresentable for view controllers. Use a Coordinator for delegates.
  • SwiftUI inside UIKit: UIHostingController for screens or parts of screens, UIHostingConfiguration for cells (iOS 16).

Most real apps mix both, so interviewers ask this often.

UIKit inside SwiftUI

struct SearchBar: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UISearchBar {
        let bar = UISearchBar()
        bar.delegate = context.coordinator
        return bar
    }

    func updateUIView(_ bar: UISearchBar, context: Context) {
        if bar.text != text { bar.text = text }
    }

    func makeCoordinator() -> Coordinator { Coordinator(text: $text) }

    final class Coordinator: NSObject, UISearchBarDelegate {
        var text: Binding<String>
        init(text: Binding<String>) { self.text = text }

        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            text.wrappedValue = searchText
        }
    }
}
MethodWhen it runsWhat to do
makeUIViewOnceCreate and configure the view
updateUIViewEvery time SwiftUI state changesPush new values into the view
makeCoordinatorOnce, before makeUIViewCreate the delegate / target object
dismantleUIViewWhen removedClean up observers
sizeThatFits (iOS 16)During layoutGive a custom size

Rules:

  • Do not create the view in updateUIView. Only update it.
  • Compare before you set (if bar.text != text). This avoids loops and cursor jumps.
  • The coordinator sends UIKit events back to SwiftUI, usually through a Binding.

A view controller

struct DocumentPicker: UIViewControllerRepresentable {
    var onPick: (URL) -> Void

    func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
        let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf])
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ vc: UIDocumentPickerViewController, context: Context) {}

    func makeCoordinator() -> Coordinator { Coordinator(onPick: onPick) }

    final class Coordinator: NSObject, UIDocumentPickerDelegate {
        let onPick: (URL) -> Void
        init(onPick: @escaping (URL) -> Void) { self.onPick = onPick }
        func documentPicker(_ c: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
            urls.first.map(onPick)
        }
    }
}

SwiftUI inside UIKit

let settings = UIHostingController(rootView: SettingsView())
navigationController?.pushViewController(settings, animated: true)

To embed it as a child:

let host = UIHostingController(rootView: BannerView(title: "Sale"))
addChild(host)
view.addSubview(host.view)
host.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    host.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
])
host.didMove(toParent: self)
host.sizingOptions = .intrinsicContentSize // iOS 16: size follows SwiftUI content

Do not forget addChild and didMove(toParent:). Without them, appearance callbacks and environment values break.

SwiftUI in cells

cell.contentConfiguration = UIHostingConfiguration {
    HStack {
        Image(systemName: "star")
        Text(item.title)
    }
}
.margins(.all, 12)

This is great for migrating a list screen step by step.

Passing data into a hosted view

The rootView is a value. To update it, set a new rootView, or share an @Observable model:

let model = BannerModel()
let host = UIHostingController(rootView: BannerView(model: model))
model.title = "New title" // view updates automatically

Newer helpers

  • iOS 18: UIGestureRecognizerRepresentable wraps UIKit gesture recognizers for SwiftUI.
  • iOS 18: UIKit can animate with SwiftUI Animation values (UIView.animate(_ animation:)).
  • iOS 26: UIKit tracks @Observable models automatically. Read a property in layoutSubviews or the new updateProperties(), and UIKit updates the view when it changes.

Common follow-up questions

  • Why use a Coordinator instead of the struct itself? The struct is recreated often. Delegates need a stable class instance.
  • How do you get the environment in updateUIView? Use context.environment.
  • Can I call SwiftUI dismiss from UIKit? Pass a closure in, or use the Coordinator to call back.
  • What breaks most often? Missing addChild, sizing of hosted views, and update loops in updateUIView.