SwiftUI-QRH

SwiftUI Quick Introduction

1) State & updates (what triggers a redraw)

@MainActor final class UserVM: ObservableObject {
    @Published var name = ""
    func load() async {
        let fetched = await api.fetchName()
        name = fetched
    }
}
struct ProfileView: View {
    @StateObject var vm = UserVM()

    var body: some View {
        Text(vm.name)
            .task { await vm.load() }
            .onChange(of: vm.name) { _ in /* side effect */ }
    }
}

2) Calling “traditional” APIs from SwiftUI

actor LocationService: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    private var cont: CheckedContinuation<CLLocation, Never>?

    func requestOnce() async -> CLLocation {
        manager.delegate = self
        manager.requestWhenInUseAuthorization()
        manager.requestLocation()
        return await withCheckedContinuation { cont in self.cont = cont }
    }

    nonisolated func locationManager(_ m: CLLocationManager, didUpdateLocations locs: [CLLocation]) {
        Task { await cont?.resume(returning: locs.last!) }
    }
    nonisolated func locationManager(_ m: CLLocationManager, didFailWithError _: Error) {
        Task { await cont?.resume(returning: .init()) }
    }
}

@MainActor final class LocationVM: ObservableObject {
    @Published var location: CLLocation?
    private let svc = LocationService()
    func fetch() async { location = await svc.requestOnce() }
}
struct NetworkView: View {
    @State private var text = ""
    let pub: AnyPublisher<String, Never>

    var body: some View {
        Text(text)
            .onReceive(pub) { text = $0 }
    }
}

3) Bridging UIKit <-> SwiftUI

A) Use a UIKit view inside SwiftUI

struct AttributedLabel: UIViewRepresentable {
    var text: NSAttributedString

    func makeUIView(context: Context) -> UILabel {
        let l = UILabel()
        l.numberOfLines = 0
        return l
    }
    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.attributedText = text
    }
}

B) Use SwiftUI inside UIKit/XIBs

final class HostVC: UIViewController {
    @IBOutlet weak var container: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        let child = UIHostingController(rootView: SettingsView())
        addChild(child)
        child.view.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(child.view)
        NSLayoutConstraint.activate([
            child.view.topAnchor.constraint(equalTo: container.topAnchor),
            child.view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            child.view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            child.view.bottomAnchor.constraint(equalTo: container.bottomAnchor)
        ])
        child.didMove(toParent: self)
    }
}
cell.contentConfiguration = UIHostingConfiguration {
    HStack {
        Image(systemName: "video")
        Text(model.title)
    }
}

4) Coordinators (delegates & callbacks)

When wrapping a UIKit control that uses delegates, forward events via Coordinator:

struct TextFieldUIKit: UIViewRepresentable {
    @Binding var text: String

    class Coordinator: NSObject, UITextFieldDelegate {
        var parent: TextFieldUIKit
        init(_ parent: TextFieldUIKit) { self.parent = parent }
        func textFieldDidChangeSelection(_ tf: UITextField) {
            parent.text = tf.text ?? ""
        }
    }

    func makeCoordinator() -> Coordinator { Coordinator(self) }

    func makeUIView(context: Context) -> UITextField {
        let tf = UITextField()
        tf.delegate = context.coordinator
        tf.addTarget(context.coordinator, action: #selector(updateText(_:)), for: .editingChanged)
        return tf
    }

    @objc private func updateText(_ sender: UITextField) { /* handled by delegate */ }

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

5) List, diffing, and identity

struct Item: Identifiable, Equatable { let id: UUID; var title: String }

List(items) { item in Text(item.title) }

6) Side effects & lifecycle

.task(id: query) {
    results = await search(query)
}

7) Architecture that stays out of your way

protocol UserAPI { func profile() async throws -> Profile }
struct LiveUserAPI: UserAPI { /* URLSession code */ }

@MainActor @Observable final class ProfileVM {
    private let api: UserAPI
    var profile: Profile?
    init(api: UserAPI) { self.api = api }
    func load() async { profile = try? await api.profile() }
}
#Preview {
    ProfileView(vm: .init(api: MockUserAPI()))
}

8) Performance hints you’ll actually feel

struct ExpensiveRow: View, Equatable {
    let model: Item
    static func == (l: Self, r: Self) -> Bool { l.model == r.model }
    var body: some View { /* heavy view */ }
}

9) Animations that don’t fight you

withAnimation(.spring) { isExpanded.toggle() }

10) Common UIKit jobs in SwiftUI form

@State private var show = false
Button("Delete") { show = true }
.alert("Confirm", isPresented: $show) {
    Button("Delete", role: .destructive) { /* do it */ }
}
@FocusState private var focused: Bool
TextField("Name", text: $name).focused($focused)

11) Migrating screens incrementally from UIKit/XIBs

12) Practical checklist (at work)


© 2025 Sami Sharafeddine — Licensed under CC BY 4.0
See the full LICENSE file for details.