Course outline

iOS / Swift / Accessible systems and target evidence / Lesson 15

UIKit representables and coordinators

Bridge declarative state to UIKit without recreating controls or turning delegate updates into feedback loops.

About 5 min readContent reviewed
On this page

Lesson goals

  • Bridge a specific missing capability
  • Create once and update from current state
  • Use a coordinator for the imperative callback surface

Bridge a specific missing capability

UIViewRepresentable lets a SwiftUI hierarchy host a UIKit view. Use it when a concrete UIKit capability is needed, while keeping the adapter narrow and its ownership clear. The representable value can be recreated as SwiftUI evaluates the hierarchy, but the hosted UIView has its own managed lifecycle. Do not allocate a replacement native control during every update merely because the representable struct is new. Separate creation, configuration updates, delegate callbacks, and cleanup. A thin adapter is easier to test than a wrapper that also fetches data, performs navigation, and owns business validation.

Create once and update from current state

The first fragment implements a read-only UILabel bridge. makeUIView creates the native view and establishes stable configuration. updateUIView copies the current input into that existing view only when necessary. This division avoids treating every declarative update as a new UIKit lifetime. The text remains synthetic and the module requires SwiftUI and UIKit on a compatible iOS host. A production bridge should also review Dynamic Type, accessibility, sizing, and trait changes. A label that renders one short string does not establish a complete adaptive interface.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
import SwiftUI
import UIKit
@MainActor
struct NativeLabel: UIViewRepresentable {
    let text: String
    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.numberOfLines = 0
        return label
    }
    func updateUIView(_ label: UILabel, context: Context) {
        if label.text != text { label.text = text }
    }
}
Expected output
The existing UILabel reflects the current text input without being recreated for each update.

The application host and sizing integration are omitted. No UIKit view was created during authoring.

Use a coordinator for the imperative callback surface

A coordinator can implement UIKit delegate protocols and route native events into a SwiftUI binding or command. It should not accidentally retain a dead screen through a long-lived global observer. The second fragment coordinates a UITextView draft. updateUIView refreshes the coordinator’s representable value so callbacks use the current binding contract, then avoids unnecessary text replacement. UIKit text editing includes selection and marked text for input methods; real editors need more careful synchronization than indiscriminate assignment on every update. Keep the example’s limited scope explicit.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
@MainActor
struct NativeDraft: UIViewRepresentable {
    @Binding var text: String
    func makeCoordinator() -> Coordinator { Coordinator(self) }
    func makeUIView(context: Context) -> UITextView {
        let view = UITextView()
        view.delegate = context.coordinator
        return view
    }
    func updateUIView(_ view: UITextView, context: Context) {
        context.coordinator.parent = self
        if view.text != text { view.text = text }
    }
    static func dismantleUIView(_ view: UITextView, coordinator: Coordinator) {
        view.delegate = nil
    }
    @MainActor final class Coordinator: NSObject, UITextViewDelegate {
        var parent: NativeDraft
        init(_ parent: NativeDraft) { self.parent = parent }
        func textViewDidChange(_ textView: UITextView) {
            if parent.text != textView.text { parent.text = textView.text }
        }
    }
}
Expected output
Native edits update the binding; external text changes update the existing UITextView.

Requires the preceding imports. This is a limited draft bridge, not a full editor implementation or a tested input-method contract.

Prevent feedback loops without hiding legitimate edits

A native callback can update SwiftUI state, which schedules another representable update. If every update writes native state and triggers another callback, a feedback loop or repeated selection reset can follow. Equality checks help with simple text, but complex controls may require an explicit distinction between programmatic and user-driven changes. Do not suppress all callbacks globally because that can drop real edits. Keep validation at the domain command boundary rather than rewriting text unpredictably during composition. Test rapid edits, external resets, and cancellation under the documented draft policy.

Clean up the resources actually owned by the bridge

dismantleUIView is a place to undo adapter-owned registrations and disconnect callbacks when the hosted view is removed. A parent-owned model should not be disposed merely because a temporary representable disappears. Likewise, any notification observer, timer, or task started by the adapter needs an explicit stop owner. Respect UIKit’s main-thread or main-actor requirements under the selected SDK and language mode. Do not move a view into a detached task to silence a concurrency warning. Transfer immutable data across concurrency boundaries and update native UI through its supported isolation.

Verify both directions and removal

In an authorized SDK host, test initial rendering, a user edit flowing outward, an external value flowing inward, repeated updates without edits, and removal while callbacks are possible. Add Arabic input, marked-text composition, selection preservation, Dynamic Type, and accessibility checks for a real text editor. A pure binding test cannot establish UIKit delegate timing or keyboard behavior. This course does not execute representables, create native views, or claim simulator verification. The code blocks remain explicit host fragments so their expected behavior cannot be mistaken for recorded device evidence.

Common errors and fixes

A native control is recreated during every update

Why it happens
Creation and state synchronization were conflated.
How to fix it
Create in makeUIView and update the managed instance in updateUIView.
Prevent it next time
Exercise repeated parent updates.

Typing resets selection or repeats callbacks

Why it happens
Programmatic text assignment was unconditional.
How to fix it
Synchronize only necessary changes and account for composition and selection.
Prevent it next time
Test native editing alongside external updates.

A removed bridge still receives observer work

Why it happens
Adapter-owned registrations were never disconnected.
How to fix it
Clean up the owned observer, task, or delegate at its lifecycle boundary.
Prevent it next time
Remove the view while work is pending.

Check your understanding

1. Why does the coordinator replace parent with the latest representable value?

Show worked solution

The representable struct can be recreated with a different binding or configuration while the coordinator persists. Updating the stored value keeps callback routing aligned with current inputs. It does not justify retaining an old screen or using the coordinator as a global service.

2. Why is the sample insufficient for a complete multilingual editor?

Show worked solution

It demonstrates two-way text synchronization, but marked text, selection, undo behavior, layout, keyboard commands, and accessibility require additional design and target tests. Treat those as explicit requirements rather than assuming a basic delegate callback establishes them.

Key takeaways

  • Bridge a specific missing capability
  • Verify both directions and removal

Official references