swiftui-uikit-interop
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI-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 for complete wrapping recipes and for UIKit-to-SwiftUI migration patterns.
references/representable-recipes.mdreferences/hosting-migration.md实现UIKit与SwiftUI的双向桥接。封装UIKit视图和视图控制器供SwiftUI使用,将SwiftUI视图嵌入UIKit页面,并跨边界同步状态。本指南基于iOS 26+和Swift 6.2特性编写;除非另有说明,所有内容均向后兼容到iOS 16。
完整的封装示例请查看,UIKit到SwiftUI的迁移模式请查看。
references/representable-recipes.mdreferences/hosting-migration.mdUIViewRepresentable Protocol
UIViewRepresentable协议
Use to wrap any subclass for use in SwiftUI.
UIViewRepresentableUIView使用封装任意子类,使其可以在SwiftUI中使用。
UIViewRepresentableUIViewRequired 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
生命周期时序
| Method | When Called | Purpose |
|---|---|---|
| Before | Create the delegate/datasource reference type. |
| Once, when the representable enters the view tree. | Allocate and configure the UIKit view. |
| Immediately after | Push SwiftUI state into the UIKit view. |
| When the representable is removed from the view tree. | Clean up observers, timers, subscriptions. |
| During layout, when SwiftUI needs the view's ideal size. iOS 16+. | Return a custom size proposal. |
Why is the most important method: SwiftUI calls it every time any , , , or 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.
updateUIView@Binding@State@Environment@Observable| 方法 | 调用时机 | 用途 |
|---|---|---|
| 在 | 创建代理/数据源引用类型实例 |
| 当Representable进入视图树时调用一次 | 分配并配置UIKit视图 |
| 紧随 | 将SwiftUI状态同步到UIKit视图 |
| 当Representable从视图树移除时调用 | 清理观察者、定时器、订阅 |
| 布局阶段,SwiftUI需要获取视图理想尺寸时调用,仅支持iOS 16+ | 返回自定义尺寸建议 |
为什么是最重要的方法: 只要Representable读取的任意、、或属性发生变更,SwiftUI就会调用该方法。所有从SwiftUI到UIKit的状态同步都在这里完成,如果遗漏了某个属性,UIKit视图就会出现状态不同步的问题。
updateUIView@Binding@State@Environment@ObservableOptional: 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 to wrap a subclass -- typically for system pickers, document scanners, mail compose, or any controller that presents modally.
UIViewControllerRepresentableUIViewControllerswift
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) }
}使用封装子类,通常用于系统选择器、文档扫描仪、邮件编写界面或任何模态弹出的控制器。
UIViewControllerRepresentableUIViewControllerswift
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 or closures:
@Bindingswift
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会捕获代理回调,并通过父类的或闭包将结果传回SwiftUI:
@Bindingswift
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 (). SwiftUI representable structs are value types and cannot serve as delegates. The Coordinator is a instance that SwiftUI creates and manages for you -- it lives as long as the representable view.
classclassUIKit代理、数据源和目标-动作模式需要引用类型(),而SwiftUI的Representable是值类型,无法作为代理使用。Coordinator是SwiftUI为你创建和管理的实例,它的生命周期与Representable视图一致。
classclassStructure
结构规范
Always nest the Coordinator inside the representable or in an extension. Store a reference to (the representable struct) so the coordinator can write back to properties.
parent@Bindingswift
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内部或其扩展中,存储指向(Representable结构体)的引用,这样Coordinator就可以向属性回写数据。
parent@Bindingswift
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
核心规则
-
Set the delegate in/
makeUIView, never inmakeUIViewController. The update method runs on every state change -- setting the delegate there causes redundant assignment and can trigger unexpected side effects.updateUIView -
The coordinator'sproperty is updated automatically. SwiftUI updates the coordinator's reference to the latest representable struct value before each call to
parent. This means the coordinator always sees currentupdateUIViewvalues through@Binding.parent -
Usein closures to avoid retain cycles between the coordinator and UIKit objects that capture it.
[weak coordinator]
-
在/
makeUIView中设置代理,绝对不要在makeUIViewController中设置。 更新方法会在每次状态变更时运行,在该方法中设置代理会导致重复赋值,还可能触发意外的副作用。updateUIView -
Coordinator的属性会自动更新。 每次调用
parent之前,SwiftUI都会将Coordinator指向最新的Representable结构体实例,确保Coordinator始终可以通过updateUIView获取到最新的parent值。@Binding -
在闭包中使用避免Coordinator和捕获它的UIKit对象之间产生循环引用。
[weak coordinator]
UIHostingController
UIHostingController
Embed SwiftUI views inside UIKit view controllers using .
UIHostingController使用将SwiftUI视图嵌入到UIKit视图控制器中。
UIHostingControllerBasic 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]| Option | Effect |
|---|---|
| The hosting controller's view reports its SwiftUI content size as |
| Updates |
swift
@available(iOS 16.0, *)
hostingController.sizingOptions = [.intrinsicContentSize]| 选项 | 效果 |
|---|---|
| 宿主控制器的视图将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 object and SwiftUI tracks changes automatically -- no need to reassign .
@ObservablerootView当UIKit中的数据发生变更时,将新状态推入托管的SwiftUI视图:
swift
func updateProfile(_ profile: Profile) {
hostingController.rootView = ProfileView(profile: profile)
}对于可观测模型,直接传递对象即可,SwiftUI会自动跟踪变更,无需重新赋值。
@ObservablerootViewUIHostingConfiguration (iOS 16+)
UIHostingConfiguration(iOS 16+)
Render SwiftUI content directly inside or without managing a child hosting controller:
UICollectionViewCellUITableViewCellswift
@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
}无需管理子宿主控制器,直接在或中渲染SwiftUI内容:
UICollectionViewCellUITableViewCellswift
@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 communicate their natural size to SwiftUI through . SwiftUI respects this during layout unless overridden by or .
UIViewRepresentableintrinsicContentSizeframe()fixedSize()封装在中的UIKit视图通过将其自然尺寸传递给SwiftUI,除非被或覆盖,否则SwiftUI在布局时会尊重该尺寸。
UIViewRepresentableintrinsicContentSizeframe()fixedSize()fixedSize() and frame() Interactions
fixedSize()和frame()的交互效果
| SwiftUI Modifier | Effect on Representable |
|---|---|
| No modifier | SwiftUI uses |
| Forces the representable to its ideal (intrinsic) size in both axes. |
| Fixes width to intrinsic; height remains flexible. |
| Overrides the proposed size; UIKit view receives this size. |
| SwiftUI修饰符 | 对Representable的影响 |
|---|---|
| 无修饰符 | SwiftUI使用 |
| 强制Representable在两个轴上都使用理想(固有)尺寸 |
| 宽度固定为固有尺寸,高度保持可伸缩 |
| 覆盖建议尺寸,UIKit视图将使用该尺寸 |
Auto Layout with UIHostingController
配合UIHostingController使用Auto Layout
When embedding as a child, pin its view with constraints. Use 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内容的自然尺寸,实现自适应尺寸的单元格或可变高度的区块。
UIHostingController.sizingOptions = [.intrinsicContentSize]State Synchronization Patterns
状态同步模式
@Binding: Two-Way Sync (SwiftUI <-> UIKit)
@Binding:双向同步(SwiftUI <-> UIKit)
Use when both sides read and write the same value. The coordinator writes to in delegate callbacks; reads the binding and pushes it into the UIKit view.
@Bindingparent.bindingPropertyupdateUIViewswift
// 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
}当双方都需要读写同一个值时使用。Coordinator在代理回调中写入,读取绑定值并同步到UIKit视图。
@Bindingparent.bindingPropertyupdateUIViewswift
// 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.environmentswift
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
}通过在Representable方法中访问SwiftUI环境值:
context.environmentswift
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@Bindingswift
func updateUIView(_ uiView: UITextView, context: Context) {
// GUARD: Only update if values actually differ
if uiView.text != text {
uiView.text = text
}
}Without the guard, setting may trigger the delegate's , which writes to , which triggers again.
uiView.texttextViewDidChangeparent.textupdateUIView只要SwiftUI状态发生变更就会调用,包括Coordinator写入触发的状态变更。添加冗余更新防护防止无限循环:
updateUIView@Bindingswift
func updateUIView(_ uiView: UITextView, context: Context) {
// 防护:仅当值实际不同时才更新
if uiView.text != text {
uiView.text = text
}
}如果没有防护,设置可能触发代理的,写入后会再次触发,形成无限循环。
uiView.texttextViewDidChangeparent.textupdateUIViewSwift 6.2 Sendable Considerations
Swift 6.2 Sendable注意事项
UIKit delegate protocols are not . When the coordinator conforms to a UIKit delegate, it inherits main-actor isolation from UIKit. Mark coordinators or use only for methods that truly do not touch UIKit state. In Swift 6.2 with strict concurrency:
Sendable@MainActornonisolatedswift
@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 or captured on the correct actor.
@SendableUIKit代理协议不遵循。当Coordinator遵循UIKit代理时,它会从UIKit继承主Actor隔离。将Coordinator标记为,或者仅对完全不涉及UIKit状态的方法使用修饰。在启用严格并发的Swift 6.2中:
Sendable@MainActornonisolatedswift
@MainActor
final class Coordinator: NSObject, UISearchBarDelegate {
var parent: SearchBarView
init(_ parent: SearchBarView) { self.parent = parent }
// 代理方法是主Actor隔离的,可以安全访问UIKit和@Binding
}如果跨隔离边界传递闭包,确保它们是的,或者在正确的Actor上捕获。
@SendableCommon Mistakes
常见错误
DO / DON'T
正确做法 / 错误做法
DON'T: Create the UIKit view in .
DO: Create the view once in ; only configure/update it in .
Why: runs on every state change. Creating a new view each time destroys all UIKit state (selection, scroll position, first responder) and leaks memory.
updateUIViewmakeUIViewupdateUIViewupdateUIViewDON'T: Set delegates in .
DO: Set delegates in / only.
Why: Redundant delegate assignment on every update can reset internal delegate state in UIKit views like or .
updateUIViewmakeUIViewmakeUIViewControllerWKWebViewMKMapViewDON'T: Hold strong references to the Coordinator from closures.
DO: Use 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.
[weak coordinator]DON'T: Forget to call or completion handlers.
DO: Use the coordinator to track dismissal and invoke in all delegate exit paths.
Why: Modal controllers presented by SwiftUI (via ) need their dismiss binding toggled, or the sheet state becomes inconsistent.
parent.dismiss()parent.dismiss().sheetDON'T: Ignore for views that hold observers or timers.
DO: Clean up observers, subscriptions, and instances in .
Why: Without cleanup, observers and timers continue firing after the view is removed, causing crashes or stale state updates.
dismantleUIViewNotificationCenterCombineTimerdismantleUIViewDON'T: Force 's view to fill the parent without proper constraints.
DO: Use Auto Layout constraints or for proper embedding.
Why: Setting manually breaks adaptive layout, trait propagation, and safe area handling.
UIHostingControllersizingOptionsframeDON'T: Try to use in the Coordinator -- it is not a .
DO: Use regular stored properties on the Coordinator and communicate to SwiftUI via 's properties.
Why: only works inside conformances. Using it on a class has no effect.
@StateViewparent@Binding@StateViewDON'T: Skip the / dance when embedding .
DO: Always call , add the view to the hierarchy, then call .
Why: Skipping containment causes viewWillAppear/viewDidAppear to never fire, breaks trait collection propagation, and causes visual glitches.
addChilddidMove(toParent:)UIHostingControlleraddChild(_:)didMove(toParent:)❌ 错误: 在中创建UIKit视图
✅ 正确: 在中创建一次视图,仅在中配置/更新视图
原因: 会在每次状态变更时运行,每次创建新视图会丢失所有UIKit状态(选中状态、滚动位置、第一响应者)并导致内存泄漏。
updateUIViewmakeUIViewupdateUIViewupdateUIView❌ 错误: 在中设置代理
✅ 正确: 仅在/中设置代理
原因: 每次更新时重复设置代理可能会重置或等UIKit视图的内部代理状态。
updateUIViewmakeUIViewmakeUIViewControllerWKWebViewMKMapView❌ 错误: 闭包中持有Coordinator的强引用
✅ 正确: 在闭包中使用
原因: UIKit对象通常会存储闭包(完成处理程序、动作块),持有Coordinator的强引用而Coordinator又持有UIKit视图的引用会产生循环引用。
[weak coordinator]❌ 错误: 忘记调用或完成处理程序
✅ 正确: 通过Coordinator跟踪 dismiss 状态,在所有代理退出路径中调用
原因: SwiftUI通过弹出的模态控制器需要切换其dismiss绑定,否则弹窗状态会不一致。
parent.dismiss()parent.dismiss().sheet❌ 错误: 持有观察者或定时器的视图忽略
✅ 正确: 在中清理观察者、订阅和实例
原因: 不清理会导致观察者和定时器在视图移除后继续触发,造成崩溃或过时的状态更新。
dismantleUIViewdismantleUIViewNotificationCenterCombineTimer❌ 错误: 没有设置合理约束就强制的视图填充父容器
✅ 正确: 使用Auto Layout约束或实现合理嵌入
原因: 手动设置会破坏自适应布局、特性传递和安全区域处理。
UIHostingControllersizingOptionsframe❌ 错误: 在Coordinator中使用 —— 它不是
✅ 正确: 在Coordinator中使用普通存储属性,通过的属性与SwiftUI通信
原因: 仅在实现中生效,在类上使用没有任何效果。
@StateViewparent@Binding@StateView❌ 错误: 嵌入时跳过/流程
✅ 正确: 始终按顺序调用、将视图添加到层级、然后调用
原因: 跳过容器流程会导致viewWillAppear/viewDidAppear永不触发,破坏特性集合传递,造成视觉异常。
UIHostingControlleraddChilddidMove(toParent:)addChild(_:)didMove(toParent:)Review Checklist
检查清单
- View/controller created in , not
make*update* - Coordinator set as delegate in , not
make*update* - used for two-way state sync
@Binding - handles all SwiftUI state changes with redundancy guards
updateUIView - cleans up observers/timers if needed
dismantleUIView - No retain cycles between coordinator and closures ()
[weak coordinator] - properly added as child (
UIHostingController+addChild)didMove(toParent:) - Sizing strategy chosen (vs fixed
intrinsicContentSizevsframe)sizeThatFits - Environment values read in via
updateUIViewwhere neededcontext.environment - Coordinator marked for Swift 6.2 strict concurrency
@MainActor - Modal controllers dismiss in all delegate exit paths (success, cancel, error)
- used for collection/table view cells instead of manual hosting (iOS 16+)
UIHostingConfiguration
- 视图/控制器在方法中创建,而非
make*方法update* - Coordinator在方法中被设置为代理,而非
make*方法update* - 双向状态同步使用
@Binding - 处理所有SwiftUI状态变更并添加冗余更新防护
updateUIView - 如有需要,中清理了观察者/定时器
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:
- with "UIViewRepresentable", "UIViewControllerRepresentable", "UIHostingController"
searchAppleDocumentation - with
fetchAppleDocumentation,/documentation/SwiftUI/UIViewRepresentable/documentation/SwiftUI/UIHostingController
使用apple-docs MCP验证UIViewRepresentable协议要求并检查API变更:
- 调用,传入关键词"UIViewRepresentable"、"UIViewControllerRepresentable"、"UIHostingController"
searchAppleDocumentation - 调用,传入路径
fetchAppleDocumentation、/documentation/SwiftUI/UIViewRepresentable/documentation/SwiftUI/UIHostingController
References
参考资料
- Wrapping recipes:
references/representable-recipes.md - Migration patterns:
references/hosting-migration.md - Apple docs: UIViewRepresentable
- Apple docs: UIViewControllerRepresentable
- Apple docs: UIHostingController
- 封装示例:
references/representable-recipes.md - 迁移模式:
references/hosting-migration.md - Apple官方文档:UIViewRepresentable
- Apple官方文档:UIViewControllerRepresentable
- Apple官方文档:UIHostingController