swiftui-uikit-interop by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-uikit-interop实现 UIKit 与 SwiftUI 的双向桥接。封装 UIKit 视图和视图控制器以在 SwiftUI 中使用,将 SwiftUI 视图嵌入到 UIKit 屏幕中,并跨边界同步状态。目标为 iOS 26+ 及 Swift 6.2 模式;除非另有说明,否则向后兼容至 iOS 16。
完整的封装示例请参见 references/representable-recipes.md,UIKit 到 SwiftUI 的迁移模式请参见 references/hosting-migration.md。
使用 UIViewRepresentable 来封装任何 UIView 子类,以便在 SwiftUI 中使用。
struct WrappedTextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
// 当 SwiftUI 将此视图插入视图层次结构时调用一次。
// 创建并返回 UIKit 视图。一次性设置放在这里。
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = .preferredFont(forTextStyle: .body)
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
// 每次影响此视图的 SwiftUI 状态变化时都会调用。
// 将 SwiftUI 状态同步到 UIKit 视图。
// 防止冗余更新以避免循环。
if uiView.text != text {
uiView.text = text
}
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 方法 | 调用时机 | 目的 |
|---|---|---|
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 视图将失去同步。
static func dismantleUIView(_ uiView: UITextView, coordinator: Coordinator) {
// 移除观察者、使计时器失效、取消订阅。
// 传入 coordinator 以便访问存储在其上的状态。
coordinator.cancellables.removeAll()
}
@available(iOS 16.0, *)
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: UITextView,
context: Context
) -> CGSize? {
// 返回 nil 以回退到 UIKit 的 intrinsicContentSize。
// 返回 CGSize 以覆盖 SwiftUI 对此视图的尺寸计算。
let width = proposal.width ?? UIView.layoutFittingExpandedSize.width
let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
return size
}
使用 UIViewControllerRepresentable 来封装 UIViewController 子类——通常用于系统选择器、文档扫描仪、邮件撰写或任何以模态方式呈现的控制器。
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) {
// 对于模态控制器通常为空——没有需要从 SwiftUI 推送的内容。
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
}
协调器捕获委托回调,并通过父视图的 @Binding 或闭包将结果路由回 SwiftUI:
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()
}
}
}
UIKit 委托、数据源和目标-操作模式需要一个引用类型(class)。SwiftUI representable 结构体是值类型,不能作为委托。协调器是 SwiftUI 为你创建和管理的 class 实例——它的生命周期与 representable 视图一样长。
始终将协调器嵌套在 representable 内部或扩展中。存储对 parent(representable 结构体)的引用,以便协调器可以写回 @Binding 属性。
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 // 在这里设置委托,而不是在 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()
}
}
}
在 makeUIView/makeUIViewController 中设置委托,绝不要在 updateUIView 中设置。 update 方法在每次状态变化时都会运行——在那里设置委托会导致冗余赋值,并可能触发意外的副作用。
协调器的 parent 属性会自动更新。 在每次调用 updateUIView 之前,SwiftUI 会更新协调器对最新 representable 结构体值的引用。这意味着协调器始终可以通过 parent 看到当前的 @Binding 值。
在闭包中使用 [weak coordinator] 以避免协调器与捕获它的 UIKit 对象之间的循环引用。
使用 UIHostingController 将 SwiftUI 视图嵌入到 UIKit 视图控制器中。
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)是强制性的。跳过任何一步都会导致容器回调触发错误,从而破坏外观转换和特征传播。
@available(iOS 16.0, *)
hostingController.sizingOptions = [.intrinsicContentSize]
| 选项 | 效果 |
|---|---|
.intrinsicContentSize | 托管控制器的视图将其 SwiftUI 内容尺寸报告为 intrinsicContentSize。在自动布局中,当托管视图应自行调整大小时使用。 |
.preferredContentSize | 更新 preferredContentSize 以匹配 SwiftUI 内容。当以弹出框或表单形式呈现时使用。 |
当 UIKit 中的数据发生变化时,将新状态推送到托管的 SwiftUI 视图中:
func updateProfile(_ profile: Profile) {
hostingController.rootView = ProfileView(profile: profile)
}
对于可观察模型,传递一个 @Observable 对象,SwiftUI 会自动跟踪变化——无需重新分配 rootView。
直接在 UICollectionViewCell 或 UITableViewCell 内渲染 SwiftUI 内容,而无需管理子托管控制器:
@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
}
封装在 UIViewRepresentable 中的 UIKit 视图通过 intrinsicContentSize 将其自然尺寸传达给 SwiftUI。除非被 frame() 或 fixedSize() 覆盖,否则 SwiftUI 在布局期间会尊重此尺寸。
| SwiftUI 修饰符 | 对 Representable 的影响 |
|---|---|
| 无修饰符 | SwiftUI 使用 intrinsicContentSize 作为理想尺寸;视图是灵活的。 |
.fixedSize() | 强制 representable 在两个轴上达到其理想(固有)尺寸。 |
.fixedSize(horizontal: true, vertical: false) | 将宽度固定为固有值;高度保持灵活。 |
.frame(width:height:) | 覆盖建议的尺寸;UIKit 视图接收此尺寸。 |
将 UIHostingController 作为子控制器嵌入时,使用约束固定其视图。使用 .sizingOptions = [.intrinsicContentSize],以便自动布局可以查询 SwiftUI 内容的自然尺寸,用于自定尺寸单元格或可变高度部分。
当双方都读取和写入相同的值时,使用 @Binding。协调器在委托回调中写入 parent.bindingProperty;updateUIView 读取绑定并将其推送到 UIKit 视图。
// SwiftUI -> UIKit: 在 updateUIView 中
if uiView.text != text { uiView.text = text }
// UIKit -> SwiftUI: 在协调器委托方法中
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
对于一次性事件(按钮点击、搜索提交、扫描完成),传递闭包而不是绑定:
struct WebViewWrapper: UIViewRepresentable {
let url: URL
var onNavigationFinished: ((URL) -> Void)?
}
通过 context.environment 在 representable 方法中访问 SwiftUI 环境值:
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
}
每当 SwiftUI 状态发生变化时——包括由协调器写入 @Binding 触发的变化——都会调用 updateUIView。防止冗余更新以避免无限循环:
func updateUIView(_ uiView: UITextView, context: Context) {
// 防护:仅在值实际不同时更新
if uiView.text != text {
uiView.text = text
}
}
如果没有防护,设置 uiView.text 可能会触发委托的 textViewDidChange,后者写入 parent.text,从而再次触发 updateUIView。
UIKit 委托协议不是 Sendable。当协调器符合 UIKit 委托时,它会从 UIKit 继承主 actor 隔离。将协调器标记为 @MainActor,或仅对确实不接触 UIKit 状态的方法使用 nonisolated。在 Swift 6.2 的严格并发模式下:
@MainActor
final class Coordinator: NSObject, UISearchBarDelegate {
var parent: SearchBarView
init(_ parent: SearchBarView) { self.parent = parent }
// 委托方法是主 actor 隔离的——可以安全地访问 UIKit 和 @Binding。
}
如果跨隔离边界传递闭包,请确保它们是 @Sendable 或在正确的 actor 上捕获的。
不应做: 在 updateUIView 中创建 UIKit 视图。应做: 在 makeUIView 中一次性创建视图;仅在 updateUIView 中配置/更新它。原因: updateUIView 在每次状态变化时都会运行。每次都创建新视图会破坏所有 UIKit 状态(选择、滚动位置、第一响应者)并导致内存泄漏。
不应做: 在 updateUIView 中设置委托。应做: 仅在 makeUIView/makeUIViewController 中设置委托。原因: 每次更新时冗余的委托赋值可能会重置 UIKit 视图(如 WKWebView 或 MKMapView)的内部委托状态。
不应做: 在闭包中持有对协调器的强引用。应做: 在闭包中使用 [weak coordinator]。原因: UIKit 对象经常存储闭包(完成处理程序、操作块)。对协调器的强引用(该协调器又持有对 UIKit 视图的引用)会造成循环引用。
不应做: 忘记调用 parent.dismiss() 或完成处理程序。应做: 使用协调器来跟踪关闭,并在所有委托退出路径中调用 parent.dismiss()。原因: 由 SwiftUI(通过 .sheet)呈现的模态控制器需要切换其关闭绑定,否则工作表状态会变得不一致。
不应做: 对于持有观察者或计时器的视图,忽略 dismantleUIView。应做: 在 dismantleUIView 中清理 NotificationCenter 观察者、Combine 订阅和 Timer 实例。原因: 如果不清理,观察者和计时器在视图移除后仍会继续触发,导致崩溃或陈旧的状态更新。
不应做: 强制 UIHostingController 的视图在没有适当约束的情况下填充父视图。应做: 使用自动布局约束或 sizingOptions 进行正确的嵌入。原因: 手动设置 frame 会破坏自适应布局、特征传播和安全区域处理。
不应做: 尝试在协调器中使用 @State——它不是 View。应做: 在协调器上使用常规存储属性,并通过 parent 的 @Binding 属性与 SwiftUI 通信。原因: @State 仅在 View 一致性内部有效。在类上使用它没有效果。
不应做: 在嵌入 UIHostingController 时跳过 addChild/didMove(toParent:) 步骤。应做: 始终调用 addChild(_:),将视图添加到层次结构,然后调用 didMove(toParent:)。原因: 跳过容器步骤会导致 viewWillAppear/viewDidAppear 永远不会触发,破坏特征集合传播,并导致视觉故障。
make* 中创建,而不是在 update* 中make* 中设置为委托,而不是在 update* 中@Binding 进行双向状态同步updateUIView 处理所有 SwiftUI 状态变化,并带有冗余防护dismantleUIView 在需要时清理观察者/计时器[weak coordinator])UIHostingController 已正确添加为子控制器(addChild + didMove(toParent:))intrinsicContentSize vs 固定 frame vs sizeThatFits)context.environment 在 updateUIView 中读取环境值@MainActorUIHostingConfiguration 处理集合/表格视图单元格,而不是手动托管(iOS 16+)references/representable-recipes.mdreferences/hosting-migration.md每周安装量
397
代码仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex392
kimi-cli389
amp389
cline389
github-copilot389
opencode389
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.
Use UIViewRepresentable to wrap any UIView subclass for use in SwiftUI.
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
}
}
}
| Method | When Called | Purpose |
|---|---|---|
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:) |
WhyupdateUIView 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.
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()
}
@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
}
Use UIViewControllerRepresentable to wrap a UIViewController subclass -- typically for system pickers, document scanners, mail compose, or any controller that presents modally.
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) }
}
The coordinator captures delegate callbacks and routes results back to SwiftUI through the parent's @Binding or closures:
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()
}
}
}
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.
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.
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()
}
}
}
Set the delegate inmakeUIView/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.
The coordinator'sparent 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.
Use[weak coordinator] in closures to avoid retain cycles between the coordinator and UIKit objects that capture it.
Embed SwiftUI views inside UIKit view controllers using UIHostingController.
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.
@available(iOS 16.0, *)
hostingController.sizingOptions = [.intrinsicContentSize]
| Option | Effect |
|---|---|
.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. |
When data changes in UIKit, push new state into the hosted SwiftUI view:
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.
Render SwiftUI content directly inside UICollectionViewCell or UITableViewCell without managing a child hosting controller:
@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
}
UIKit views wrapped in UIViewRepresentable communicate their natural size to SwiftUI through intrinsicContentSize. SwiftUI respects this during layout unless overridden by frame() or fixedSize().
| SwiftUI Modifier | Effect on Representable |
|---|---|
| No modifier | SwiftUI 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. |
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.
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.
// 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
}
For fire-and-forget events (button tapped, search submitted, scan completed), pass a closure instead of a binding:
struct WebViewWrapper: UIViewRepresentable {
let url: URL
var onNavigationFinished: ((URL) -> Void)?
}
Access SwiftUI environment values inside representable methods via context.environment:
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
}
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:
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.
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:
@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.
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.
make*, not update*make*, not update*@Binding used for two-way state syncupdateUIView handles all SwiftUI state changes with redundancy guardsdismantleUIView cleans up observers/timers if needed[weak coordinator])UIHostingController properly added as child (addChild + )references/representable-recipes.mdreferences/hosting-migration.mdWeekly Installs
397
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex392
kimi-cli389
amp389
cline389
github-copilot389
opencode389
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
| 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. |
didMove(toParent:)intrinsicContentSize vs fixed frame vs sizeThatFits)updateUIView via context.environment where needed@MainActor for Swift 6.2 strict concurrencyUIHostingConfiguration used for collection/table view cells instead of manual hosting (iOS 16+)