swiftui-uikit-interop

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI-UIKit Interop

SwiftUI-UIKit 互操作

Bridge UIKit and SwiftUI in both directions. Wrap UIKit views and view controllers for use in SwiftUI, embed SwiftUI views inside UIKit screens, and synchronize state across the boundary. Targets iOS 26+ with Swift 6.2 patterns; notes backward-compatible to iOS 16 unless stated otherwise.
See
references/representable-recipes.md
for complete wrapping recipes and
references/hosting-migration.md
for UIKit-to-SwiftUI migration patterns.
实现UIKit与SwiftUI的双向桥接。封装UIKit视图和视图控制器供SwiftUI使用,将SwiftUI视图嵌入UIKit页面,并跨边界同步状态。本指南基于iOS 26+和Swift 6.2特性编写;除非另有说明,所有内容均向后兼容到iOS 16。
完整的封装示例请查看
references/representable-recipes.md
,UIKit到SwiftUI的迁移模式请查看
references/hosting-migration.md

UIViewRepresentable Protocol

UIViewRepresentable协议

Use
UIViewRepresentable
to wrap any
UIView
subclass for use in SwiftUI.
使用
UIViewRepresentable
封装任意
UIView
子类,使其可以在SwiftUI中使用。

Required Methods

必填方法

swift
struct 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
        }
    }
}
swift
struct 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
        }
    }
}

Lifecycle Timing

生命周期时序

MethodWhen CalledPurpose
makeCoordinator()
Before
makeUIView
. Once per representable lifetime.
Create the delegate/datasource reference type.
makeUIView(context:)
Once, when the representable enters the view tree.Allocate and configure the UIKit view.
updateUIView(_:context:)
Immediately after
makeUIView
, then on every relevant state change.
Push SwiftUI state into the UIKit view.
dismantleUIView(_:coordinator:)
When the representable is removed from the view tree.Clean up observers, timers, subscriptions.
sizeThatFits(_:uiView:context:)
During layout, when SwiftUI needs the view's ideal size. iOS 16+.Return a custom size proposal.
Why
updateUIView
is the most important method:
SwiftUI calls it every time any
@Binding
,
@State
,
@Environment
, or
@Observable
property read by the representable changes. All state synchronization from SwiftUI to UIKit happens here. If you skip a property, the UIKit view will fall out of sync.
方法调用时机用途
makeCoordinator()
makeUIView
之前调用,每个Representable实例生命周期内仅调用一次
创建代理/数据源引用类型实例
makeUIView(context:)
当Representable进入视图树时调用一次分配并配置UIKit视图
updateUIView(_:context:)
紧随
makeUIView
之后调用,之后每次相关状态变更时都会调用
将SwiftUI状态同步到UIKit视图
dismantleUIView(_:coordinator:)
当Representable从视图树移除时调用清理观察者、定时器、订阅
sizeThatFits(_:uiView:context:)
布局阶段,SwiftUI需要获取视图理想尺寸时调用,仅支持iOS 16+返回自定义尺寸建议
为什么
updateUIView
是最重要的方法:
只要Representable读取的任意
@Binding
@State
@Environment
@Observable
属性发生变更,SwiftUI就会调用该方法。所有从SwiftUI到UIKit的状态同步都在这里完成,如果遗漏了某个属性,UIKit视图就会出现状态不同步的问题。

Optional: dismantleUIView

可选方法:dismantleUIView

swift
static 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()
}
swift
static 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()
}

Optional: sizeThatFits (iOS 16+)

可选方法:sizeThatFits(iOS 16+)

swift
@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
}
swift
@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
}

UIViewControllerRepresentable Protocol

UIViewControllerRepresentable协议

Use
UIViewControllerRepresentable
to wrap a
UIViewController
subclass -- typically for system pickers, document scanners, mail compose, or any controller that presents modally.
swift
struct 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) }
}
使用
UIViewControllerRepresentable
封装
UIViewController
子类,通常用于系统选择器、文档扫描仪、邮件编写界面或任何模态弹出的控制器。
swift
struct 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) }
}

Handling Results from Presented Controllers

处理弹出控制器的返回结果

The coordinator captures delegate callbacks and routes results back to SwiftUI through the parent's
@Binding
or closures:
swift
extension 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()
        }
    }
}
Coordinator会捕获代理回调,并通过父类的
@Binding
或闭包将结果传回SwiftUI:
swift
extension 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()
        }
    }
}

The Coordinator Pattern

Coordinator模式

Why Coordinators Exist

Coordinator的作用

UIKit delegates, data sources, and target-action patterns require a reference type (
class
). SwiftUI representable structs are value types and cannot serve as delegates. The Coordinator is a
class
instance that SwiftUI creates and manages for you -- it lives as long as the representable view.
UIKit代理、数据源和目标-动作模式需要引用类型(
class
),而SwiftUI的Representable是值类型,无法作为代理使用。Coordinator是SwiftUI为你创建和管理的
class
实例,它的生命周期与Representable视图一致。

Structure

结构规范

Always nest the Coordinator inside the representable or in an extension. Store a reference to
parent
(the representable struct) so the coordinator can write back to
@Binding
properties.
swift
struct 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()
        }
    }
}
始终将Coordinator嵌套在Representable内部或其扩展中,存储指向
parent
(Representable结构体)的引用,这样Coordinator就可以向
@Binding
属性回写数据。
swift
struct 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()
        }
    }
}

Key Rules

核心规则

  1. Set the delegate in
    makeUIView
    /
    makeUIViewController
    , never in
    updateUIView
    .
    The update method runs on every state change -- setting the delegate there causes redundant assignment and can trigger unexpected side effects.
  2. The coordinator's
    parent
    property is updated automatically.
    SwiftUI updates the coordinator's reference to the latest representable struct value before each call to
    updateUIView
    . This means the coordinator always sees current
    @Binding
    values through
    parent
    .
  3. Use
    [weak coordinator]
    in closures
    to avoid retain cycles between the coordinator and UIKit objects that capture it.
  1. makeUIView
    /
    makeUIViewController
    中设置代理,绝对不要在
    updateUIView
    中设置。
    更新方法会在每次状态变更时运行,在该方法中设置代理会导致重复赋值,还可能触发意外的副作用。
  2. Coordinator的
    parent
    属性会自动更新。
    每次调用
    updateUIView
    之前,SwiftUI都会将Coordinator指向最新的Representable结构体实例,确保Coordinator始终可以通过
    parent
    获取到最新的
    @Binding
    值。
  3. 在闭包中使用
    [weak coordinator]
    避免Coordinator和捕获它的UIKit对象之间产生循环引用。

UIHostingController

UIHostingController

Embed SwiftUI views inside UIKit view controllers using
UIHostingController
.
使用
UIHostingController
将SwiftUI视图嵌入到UIKit视图控制器中。

Basic Embedding

基础嵌入方法

swift
final 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)
    }
}
The three-step sequence (addChild, add view, didMove) is mandatory. Skipping any step causes containment callbacks to misfire, which breaks appearance transitions and trait propagation.
swift
final class ProfileViewController: UIViewController {
    private let hostingController = UIHostingController(rootView: ProfileView())

    override func viewDidLoad() {
        super.viewDidLoad()

        // 1. 添加为子控制器
        addChild(hostingController)

        // 2. 添加视图并设置约束
        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. 通知子控制器添加完成
        hostingController.didMove(toParent: self)
    }
}
三步流程(addChild、添加视图、didMove)是强制要求的,跳过任何一步都会导致容器回调异常,破坏外观过渡和特性传递。

Sizing Options (iOS 16+)

尺寸选项(iOS 16+)

swift
@available(iOS 16.0, *)
hostingController.sizingOptions = [.intrinsicContentSize]
OptionEffect
.intrinsicContentSize
The hosting controller's view reports its SwiftUI content size as
intrinsicContentSize
. Use in Auto Layout when the hosted view should size itself.
.preferredContentSize
Updates
preferredContentSize
to match SwiftUI content. Use when presenting as a popover or form sheet.
swift
@available(iOS 16.0, *)
hostingController.sizingOptions = [.intrinsicContentSize]
选项效果
.intrinsicContentSize
宿主控制器的视图将SwiftUI内容尺寸作为
intrinsicContentSize
返回,当托管视图需要自适应尺寸时在Auto Layout中使用
.preferredContentSize
同步更新
preferredContentSize
以匹配SwiftUI内容尺寸,在作为弹出层或表单页面展示时使用

Updating the Root View

更新根视图

When data changes in UIKit, push new state into the hosted SwiftUI view:
swift
func updateProfile(_ profile: Profile) {
    hostingController.rootView = ProfileView(profile: profile)
}
For observable models, pass an
@Observable
object and SwiftUI tracks changes automatically -- no need to reassign
rootView
.
当UIKit中的数据发生变更时,将新状态推入托管的SwiftUI视图:
swift
func updateProfile(_ profile: Profile) {
    hostingController.rootView = ProfileView(profile: profile)
}
对于可观测模型,直接传递
@Observable
对象即可,SwiftUI会自动跟踪变更,无需重新赋值
rootView

UIHostingConfiguration (iOS 16+)

UIHostingConfiguration(iOS 16+)

Render SwiftUI content directly inside
UICollectionViewCell
or
UITableViewCell
without managing a child hosting controller:
swift
@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
}
无需管理子宿主控制器,直接在
UICollectionViewCell
UITableViewCell
中渲染SwiftUI内容:
swift
@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
}

Sizing and Layout

尺寸与布局

intrinsicContentSize Bridging

intrinsicContentSize桥接

UIKit views wrapped in
UIViewRepresentable
communicate their natural size to SwiftUI through
intrinsicContentSize
. SwiftUI respects this during layout unless overridden by
frame()
or
fixedSize()
.
封装在
UIViewRepresentable
中的UIKit视图通过
intrinsicContentSize
将其自然尺寸传递给SwiftUI,除非被
frame()
fixedSize()
覆盖,否则SwiftUI在布局时会尊重该尺寸。

fixedSize() and frame() Interactions

fixedSize()和frame()的交互效果

SwiftUI ModifierEffect on Representable
No modifierSwiftUI uses
intrinsicContentSize
as ideal size; the view is flexible.
.fixedSize()
Forces the representable to its ideal (intrinsic) size in both axes.
.fixedSize(horizontal: true, vertical: false)
Fixes width to intrinsic; height remains flexible.
.frame(width:height:)
Overrides the proposed size; UIKit view receives this size.
SwiftUI修饰符对Representable的影响
无修饰符SwiftUI使用
intrinsicContentSize
作为理想尺寸,视图可伸缩
.fixedSize()
强制Representable在两个轴上都使用理想(固有)尺寸
.fixedSize(horizontal: true, vertical: false)
宽度固定为固有尺寸,高度保持可伸缩
.frame(width:height:)
覆盖建议尺寸,UIKit视图将使用该尺寸

Auto Layout with UIHostingController

配合UIHostingController使用Auto Layout

When embedding
UIHostingController
as a child, pin its view with constraints. Use
.sizingOptions = [.intrinsicContentSize]
so Auto Layout can query the SwiftUI content's natural size for self-sizing cells or variable-height sections.
UIHostingController
作为子控制器嵌入时,使用约束固定其视图位置,配置
.sizingOptions = [.intrinsicContentSize]
让Auto Layout可以读取SwiftUI内容的自然尺寸,实现自适应尺寸的单元格或可变高度的区块。

State Synchronization Patterns

状态同步模式

@Binding: Two-Way Sync (SwiftUI <-> UIKit)

@Binding:双向同步(SwiftUI <-> UIKit)

Use
@Binding
when both sides read and write the same value. The coordinator writes to
parent.bindingProperty
in delegate callbacks;
updateUIView
reads the binding and pushes it into the UIKit view.
swift
// 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
}
当双方都需要读写同一个值时使用
@Binding
。Coordinator在代理回调中写入
parent.bindingProperty
updateUIView
读取绑定值并同步到UIKit视图。
swift
// SwiftUI -> UIKit: 在updateUIView中实现
if uiView.text != text { uiView.text = text }

// UIKit -> SwiftUI: 在Coordinator代理方法中实现
func textViewDidChange(_ textView: UITextView) {
    parent.text = textView.text
}

Closures: One-Way Events (UIKit -> SwiftUI)

闭包:单向事件(UIKit -> SwiftUI)

For fire-and-forget events (button tapped, search submitted, scan completed), pass a closure instead of a binding:
swift
struct WebViewWrapper: UIViewRepresentable {
    let url: URL
    var onNavigationFinished: ((URL) -> Void)?
}
对于触发即忘的事件(按钮点击、搜索提交、扫描完成),传递闭包代替绑定:
swift
struct WebViewWrapper: UIViewRepresentable {
    let url: URL
    var onNavigationFinished: ((URL) -> Void)?
}

Environment Values

环境值

Access SwiftUI environment values inside representable methods via
context.environment
:
swift
func 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
}
通过
context.environment
在Representable方法中访问SwiftUI环境值:
swift
func updateUIView(_ uiView: UITextView, context: Context) {
    let isEnabled = context.environment.isEnabled
    uiView.isEditable = isEnabled

    // 响应配色方案变更
    let colorScheme = context.environment.colorScheme
    uiView.backgroundColor = colorScheme == .dark ? .systemGray6 : .white
}

Avoiding Update Loops

避免更新循环

updateUIView
is called whenever SwiftUI state changes -- including changes triggered by the coordinator writing to a
@Binding
. Guard against redundant updates to prevent infinite loops:
swift
func updateUIView(_ uiView: UITextView, context: Context) {
    // GUARD: Only update if values actually differ
    if uiView.text != text {
        uiView.text = text
    }
}
Without the guard, setting
uiView.text
may trigger the delegate's
textViewDidChange
, which writes to
parent.text
, which triggers
updateUIView
again.
只要SwiftUI状态发生变更就会调用
updateUIView
,包括Coordinator写入
@Binding
触发的状态变更。添加冗余更新防护防止无限循环:
swift
func updateUIView(_ uiView: UITextView, context: Context) {
    // 防护:仅当值实际不同时才更新
    if uiView.text != text {
        uiView.text = text
    }
}
如果没有防护,设置
uiView.text
可能触发代理的
textViewDidChange
,写入
parent.text
后会再次触发
updateUIView
,形成无限循环。

Swift 6.2 Sendable Considerations

Swift 6.2 Sendable注意事项

UIKit delegate protocols are not
Sendable
. When the coordinator conforms to a UIKit delegate, it inherits main-actor isolation from UIKit. Mark coordinators
@MainActor
or use
nonisolated
only for methods that truly do not touch UIKit state. In Swift 6.2 with strict concurrency:
swift
@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.
}
If passing closures across isolation boundaries, ensure they are
@Sendable
or captured on the correct actor.
UIKit代理协议不遵循
Sendable
。当Coordinator遵循UIKit代理时,它会从UIKit继承主Actor隔离。将Coordinator标记为
@MainActor
,或者仅对完全不涉及UIKit状态的方法使用
nonisolated
修饰。在启用严格并发的Swift 6.2中:
swift
@MainActor
final class Coordinator: NSObject, UISearchBarDelegate {
    var parent: SearchBarView
    init(_ parent: SearchBarView) { self.parent = parent }
    // 代理方法是主Actor隔离的,可以安全访问UIKit和@Binding
}
如果跨隔离边界传递闭包,确保它们是
@Sendable
的,或者在正确的Actor上捕获。

Common Mistakes

常见错误

DO / DON'T

正确做法 / 错误做法

DON'T: Create the UIKit view in
updateUIView
. DO: Create the view once in
makeUIView
; only configure/update it in
updateUIView
. Why:
updateUIView
runs on every state change. Creating a new view each time destroys all UIKit state (selection, scroll position, first responder) and leaks memory.
DON'T: Set delegates in
updateUIView
. DO: Set delegates in
makeUIView
/
makeUIViewController
only. Why: Redundant delegate assignment on every update can reset internal delegate state in UIKit views like
WKWebView
or
MKMapView
.
DON'T: Hold strong references to the Coordinator from closures. DO: Use
[weak coordinator]
in closures. Why: UIKit objects often store closures (completion handlers, action blocks). A strong reference to the coordinator that holds a reference to the UIKit view creates a retain cycle.
DON'T: Forget to call
parent.dismiss()
or completion handlers. DO: Use the coordinator to track dismissal and invoke
parent.dismiss()
in all delegate exit paths. Why: Modal controllers presented by SwiftUI (via
.sheet
) need their dismiss binding toggled, or the sheet state becomes inconsistent.
DON'T: Ignore
dismantleUIView
for views that hold observers or timers. DO: Clean up
NotificationCenter
observers,
Combine
subscriptions, and
Timer
instances in
dismantleUIView
. Why: Without cleanup, observers and timers continue firing after the view is removed, causing crashes or stale state updates.
DON'T: Force
UIHostingController
's view to fill the parent without proper constraints. DO: Use Auto Layout constraints or
sizingOptions
for proper embedding. Why: Setting
frame
manually breaks adaptive layout, trait propagation, and safe area handling.
DON'T: Try to use
@State
in the Coordinator -- it is not a
View
. DO: Use regular stored properties on the Coordinator and communicate to SwiftUI via
parent
's
@Binding
properties. Why:
@State
only works inside
View
conformances. Using it on a class has no effect.
DON'T: Skip the
addChild
/
didMove(toParent:)
dance when embedding
UIHostingController
. DO: Always call
addChild(_:)
, add the view to the hierarchy, then call
didMove(toParent:)
. Why: Skipping containment causes viewWillAppear/viewDidAppear to never fire, breaks trait collection propagation, and causes visual glitches.
错误:
updateUIView
中创建UIKit视图 ✅ 正确:
makeUIView
中创建一次视图,仅在
updateUIView
中配置/更新视图 原因:
updateUIView
会在每次状态变更时运行,每次创建新视图会丢失所有UIKit状态(选中状态、滚动位置、第一响应者)并导致内存泄漏。
错误:
updateUIView
中设置代理 ✅ 正确: 仅在
makeUIView
/
makeUIViewController
中设置代理 原因: 每次更新时重复设置代理可能会重置
WKWebView
MKMapView
等UIKit视图的内部代理状态。
错误: 闭包中持有Coordinator的强引用 ✅ 正确: 在闭包中使用
[weak coordinator]
原因: UIKit对象通常会存储闭包(完成处理程序、动作块),持有Coordinator的强引用而Coordinator又持有UIKit视图的引用会产生循环引用。
错误: 忘记调用
parent.dismiss()
或完成处理程序 ✅ 正确: 通过Coordinator跟踪 dismiss 状态,在所有代理退出路径中调用
parent.dismiss()
原因: SwiftUI通过
.sheet
弹出的模态控制器需要切换其dismiss绑定,否则弹窗状态会不一致。
错误: 持有观察者或定时器的视图忽略
dismantleUIView
正确:
dismantleUIView
中清理
NotificationCenter
观察者、
Combine
订阅和
Timer
实例 原因: 不清理会导致观察者和定时器在视图移除后继续触发,造成崩溃或过时的状态更新。
错误: 没有设置合理约束就强制
UIHostingController
的视图填充父容器 ✅ 正确: 使用Auto Layout约束或
sizingOptions
实现合理嵌入 原因: 手动设置
frame
会破坏自适应布局、特性传递和安全区域处理。
错误: 在Coordinator中使用
@State
—— 它不是
View
正确: 在Coordinator中使用普通存储属性,通过
parent
@Binding
属性与SwiftUI通信 原因:
@State
仅在
View
实现中生效,在类上使用没有任何效果。
错误: 嵌入
UIHostingController
时跳过
addChild
/
didMove(toParent:)
流程 ✅ 正确: 始终按顺序调用
addChild(_:)
、将视图添加到层级、然后调用
didMove(toParent:)
原因: 跳过容器流程会导致viewWillAppear/viewDidAppear永不触发,破坏特性集合传递,造成视觉异常。

Review Checklist

检查清单

  • View/controller created in
    make*
    , not
    update*
  • Coordinator set as delegate in
    make*
    , not
    update*
  • @Binding
    used for two-way state sync
  • updateUIView
    handles all SwiftUI state changes with redundancy guards
  • dismantleUIView
    cleans up observers/timers if needed
  • No retain cycles between coordinator and closures (
    [weak coordinator]
    )
  • UIHostingController
    properly added as child (
    addChild
    +
    didMove(toParent:)
    )
  • Sizing strategy chosen (
    intrinsicContentSize
    vs fixed
    frame
    vs
    sizeThatFits
    )
  • Environment values read in
    updateUIView
    via
    context.environment
    where needed
  • Coordinator marked
    @MainActor
    for Swift 6.2 strict concurrency
  • Modal controllers dismiss in all delegate exit paths (success, cancel, error)
  • UIHostingConfiguration
    used for collection/table view cells instead of manual hosting (iOS 16+)
  • 视图/控制器在
    make*
    方法中创建,而非
    update*
    方法
  • Coordinator在
    make*
    方法中被设置为代理,而非
    update*
    方法
  • 双向状态同步使用
    @Binding
  • updateUIView
    处理所有SwiftUI状态变更并添加冗余更新防护
  • 如有需要,
    dismantleUIView
    中清理了观察者/定时器
  • Coordinator和闭包之间没有循环引用(使用
    [weak coordinator]
  • UIHostingController
    正确添加为子控制器(
    addChild
    +
    didMove(toParent:)
  • 已选择尺寸策略(
    intrinsicContentSize
    /固定
    frame
    /
    sizeThatFits
  • 如有需要,在
    updateUIView
    中通过
    context.environment
    读取环境值
  • 适配Swift 6.2严格并发,Coordinator标记为
    @MainActor
  • 模态控制器在所有代理退出路径(成功、取消、错误)中都执行了dismiss
  • iOS 16+环境下,列表/集合视图单元格使用
    UIHostingConfiguration
    而非手动托管

MCP Integration

MCP集成

Use the apple-docs MCP to verify UIViewRepresentable protocol requirements and check for API changes:
  • searchAppleDocumentation
    with "UIViewRepresentable", "UIViewControllerRepresentable", "UIHostingController"
  • fetchAppleDocumentation
    with
    /documentation/SwiftUI/UIViewRepresentable
    ,
    /documentation/SwiftUI/UIHostingController
使用apple-docs MCP验证UIViewRepresentable协议要求并检查API变更:
  • 调用
    searchAppleDocumentation
    ,传入关键词"UIViewRepresentable"、"UIViewControllerRepresentable"、"UIHostingController"
  • 调用
    fetchAppleDocumentation
    ,传入路径
    /documentation/SwiftUI/UIViewRepresentable
    /documentation/SwiftUI/UIHostingController

References

参考资料