axiom-uikit-bridging
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUIKit-SwiftUI Bridging
UIKit与SwiftUI桥接
Systematic guidance for bridging UIKit and SwiftUI. Most production iOS apps need both — this skill teaches the bridging patterns themselves, not the domain-specific views being bridged.
这是一份关于UIKit与SwiftUI桥接的系统性指南。大多数生产环境中的iOS应用都会同时用到这两种框架——本技能专注于讲解桥接模式本身,而非特定领域的视图桥接。
Decision Framework
决策框架
dot
digraph bridge {
start [label="What are you bridging?" shape=diamond];
start -> "UIViewRepresentable" [label="UIView subclass → SwiftUI"];
start -> "UIViewControllerRepresentable" [label="UIViewController → SwiftUI"];
start -> "UIGestureRecognizerRepresentable" [label="UIGestureRecognizer → SwiftUI\n(iOS 18+)"];
start -> "UIHostingController" [label="SwiftUI view → UIKit"];
start -> "UIHostingConfiguration" [label="SwiftUI in UIKit cell\n(iOS 16+)"];
"UIViewRepresentable" [shape=box];
"UIViewControllerRepresentable" [shape=box];
"UIGestureRecognizerRepresentable" [shape=box];
"UIHostingController" [shape=box];
"UIHostingConfiguration" [shape=box];
}Quick rules:
- Wrapping a →
UIView(Part 1)UIViewRepresentable - Wrapping a →
UIViewController(Part 2)UIViewControllerRepresentable - Wrapping a subclass →
UIGestureRecognizer(Part 2b, iOS 18+)UIGestureRecognizerRepresentable - Embedding SwiftUI in UIKit navigation → (Part 3)
UIHostingController - SwiftUI in UICollectionView/UITableView cells → (Part 3)
UIHostingConfiguration - Sharing state between UIKit and SwiftUI → shared model (Part 4)
@Observable
dot
digraph bridge {
start [label="What are you bridging?" shape=diamond];
start -> "UIViewRepresentable" [label="UIView subclass → SwiftUI"];
start -> "UIViewControllerRepresentable" [label="UIViewController → SwiftUI"];
start -> "UIGestureRecognizerRepresentable" [label="UIGestureRecognizer → SwiftUI\n(iOS 18+)"];
start -> "UIHostingController" [label="SwiftUI view → UIKit"];
start -> "UIHostingConfiguration" [label="SwiftUI in UIKit cell\n(iOS 16+)"];
"UIViewRepresentable" [shape=box];
"UIViewControllerRepresentable" [shape=box];
"UIGestureRecognizerRepresentable" [shape=box];
"UIHostingController" [shape=box];
"UIHostingConfiguration" [shape=box];
}快速规则:
- 封装→ 使用
UIView(第1部分)UIViewRepresentable - 封装→ 使用
UIViewController(第2部分)UIViewControllerRepresentable - 封装子类 → 使用
UIGestureRecognizer(第2b部分,iOS 18+)UIGestureRecognizerRepresentable - 在UIKit导航中嵌入SwiftUI → 使用(第3部分)
UIHostingController - 在UICollectionView/UITableView单元格中使用SwiftUI → 使用(第3部分)
UIHostingConfiguration - 在UIKit与SwiftUI之间共享状态 → 使用共享模型(第4部分)
@Observable
Part 1: UIViewRepresentable — Wrapping UIViews
第1部分:UIViewRepresentable — 封装UIViews
Use when you have a subclass (MKMapView, WKWebView, custom drawing views) and need it in SwiftUI.
UIViewFor comprehensive MapKit patterns and the SwiftUI Map vs MKMapView decision, see.axiom-mapkit
当你拥有子类(如MKMapView、WKWebView、自定义绘图视图)并需要在SwiftUI中使用时,可采用此方案。
UIView如需了解完整的MapKit模式,以及SwiftUI Map与MKMapView的选择决策,请查看。axiom-mapkit
Lifecycle
生命周期
makeUIView(context:) → Called ONCE. Create and configure the view.
updateUIView(_:context:) → Called on EVERY SwiftUI state change. Patch, don't recreate.
dismantleUIView(_:coordinator:) → Called when removed from hierarchy. Clean up observers/timers.Critical: is called frequently. Guard against unnecessary work:
updateUIViewswift
struct MapView: UIViewRepresentable {
let region: MKCoordinateRegion
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
return map
}
func updateUIView(_ map: MKMapView, context: Context) {
// ✅ Guard: only update if region actually changed
if map.region.center.latitude != region.center.latitude
|| map.region.center.longitude != region.center.longitude {
map.setRegion(region, animated: true)
}
}
static func dismantleUIView(_ map: MKMapView, coordinator: Coordinator) {
map.removeAnnotations(map.annotations)
}
}makeUIView(context:) → 仅调用一次。用于创建和配置视图。
updateUIView(_:context:) → 每次SwiftUI状态变更时调用。仅做增量更新,不要重新创建视图。
dismantleUIView(_:coordinator:) → 当视图从层级中移除时调用。用于清理观察者/计时器。关键注意点:会被频繁调用,需避免不必要的操作:
updateUIViewswift
struct MapView: UIViewRepresentable {
let region: MKCoordinateRegion
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
return map
}
func updateUIView(_ map: MKMapView, context: Context) {
// ✅ 仅在区域实际变化时才更新
if map.region.center.latitude != region.center.latitude
|| map.region.center.longitude != region.center.longitude {
map.setRegion(region, animated: true)
}
}
static func dismantleUIView(_ map: MKMapView, coordinator: Coordinator) {
map.removeAnnotations(map.annotations)
}
}State Synchronization
状态同步
State flows in two directions across the bridge:
SwiftUI → UIKit: Via . SwiftUI state changes trigger this method.
updateUIViewUIKit → SwiftUI: Via the Coordinator, using on the parent struct.
@Bindingswift
struct SearchField: UIViewRepresentable {
@Binding var text: String
@Binding var isEditing: Bool
func makeUIView(context: Context) -> UISearchBar {
let bar = UISearchBar()
bar.delegate = context.coordinator
return bar
}
func updateUIView(_ bar: UISearchBar, context: Context) {
bar.text = text // SwiftUI → UIKit
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UISearchBarDelegate {
var parent: SearchField
init(_ parent: SearchField) { self.parent = parent }
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
parent.text = searchText // UIKit → SwiftUI
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
parent.isEditing = true // UIKit → SwiftUI
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
parent.isEditing = false
}
}
}状态在桥接层有两种流动方向:
SwiftUI → UIKit:通过方法。SwiftUI状态变更会触发该方法。
updateUIViewUIKit → SwiftUI:通过Coordinator(协调器),使用父结构体的属性。
@Bindingswift
struct SearchField: UIViewRepresentable {
@Binding var text: String
@Binding var isEditing: Bool
func makeUIView(context: Context) -> UISearchBar {
let bar = UISearchBar()
bar.delegate = context.coordinator
return bar
}
func updateUIView(_ bar: UISearchBar, context: Context) {
bar.text = text // SwiftUI → UIKit
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UISearchBarDelegate {
var parent: SearchField
init(_ parent: SearchField) { self.parent = parent }
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
parent.text = searchText // UIKit → SwiftUI
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
parent.isEditing = true // UIKit → SwiftUI
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
parent.isEditing = false
}
}
}Layout Property Warning
布局属性警告
SwiftUI owns the layout of representable views. Never modify , , , or on the wrapped UIView — this is undefined behavior per Apple documentation. SwiftUI sets these properties during its layout pass. If you need custom sizing, override on the UIView or use .
centerboundsframetransformintrinsicContentSizesizeThatFits(_:)SwiftUI拥有可表示视图的布局控制权。绝对不要修改封装后UIView的、、或属性——根据Apple文档,这属于未定义行为。SwiftUI会在布局过程中设置这些属性。如果需要自定义尺寸,请在UIView上重写,或使用方法。
centerboundsframetransformintrinsicContentSizesizeThatFits(_:)Coordinator Pattern
协调器模式
The Coordinator is a reference type () that:
class- Acts as the delegate/data source for the UIKit view
- Holds a reference to the parent struct
UIViewRepresentable - Bridges UIKit callbacks back to SwiftUI properties
@Binding
makeCoordinator()Why not closures? Closures capture and create retain cycles. The Coordinator pattern gives you a stable reference type that SwiftUI manages.
selfswift
// ❌ Closure-based: retain cycle risk, no delegate protocol support
func makeUIView(context: Context) -> UITextField {
let field = UITextField()
field.addTarget(self, action: #selector(textChanged), for: .editingChanged) // Won't compile — self is a struct
return field
}
// ✅ Coordinator: clean lifecycle, delegate support
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UITextFieldDelegate {
var parent: SearchField
init(_ parent: SearchField) { self.parent = parent }
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}Coordinator(协调器)是引用类型(),主要作用如下:
class- 作为UIKit视图的代理/数据源
- 持有父结构体的引用
UIViewRepresentable - 将UIKit回调桥接到SwiftUI的属性
@Binding
makeCoordinator()为什么不使用闭包? 闭包会捕获并导致循环引用。协调器模式提供了一个由SwiftUI管理的稳定引用类型。
selfswift
// ❌ 闭包方式:存在循环引用风险,不支持代理协议
func makeUIView(context: Context) -> UITextField {
let field = UITextField()
field.addTarget(self, action: #selector(textChanged), for: .editingChanged) // 编译失败——self是结构体
return field
}
// ✅ 协调器方式:生命周期清晰,支持代理
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UITextFieldDelegate {
var parent: SearchField
init(_ parent: SearchField) { self.parent = parent }
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}Sizing
尺寸控制
UIViewRepresentable views participate in SwiftUI layout. Control sizing with:
swift
// If the UIView has intrinsicContentSize, SwiftUI respects it
// For views without intrinsic size (MKMapView, WKWebView), set a frame:
MapView(region: region)
.frame(height: 300)
// For views that should size to fit their content:
WrappedLabel(text: "Hello")
.fixedSize() // Uses intrinsicContentSizeOverride for custom size proposals:
sizeThatFits(_:)swift
struct WrappedLabel: UIViewRepresentable {
let text: String
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
return label
}
func updateUIView(_ label: UILabel, context: Context) {
label.text = text
}
// Custom size proposal — SwiftUI calls this during layout
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UILabel, context: Context) -> CGSize? {
let width = proposal.width ?? UIView.layoutFittingCompressedSize.width
return uiView.systemLayoutSizeFitting(
CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
}
}UIViewRepresentable视图会参与SwiftUI的布局流程。可通过以下方式控制尺寸:
swift
// 如果UIView有intrinsicContentSize,SwiftUI会自动遵循该尺寸
// 对于没有固有尺寸的视图(如MKMapView、WKWebView),设置frame:
MapView(region: region)
.frame(height: 300)
// 对于需要自适应内容尺寸的视图:
WrappedLabel(text: "Hello")
.fixedSize() // 使用intrinsicContentSize重写以自定义尺寸适配:
sizeThatFits(_:)swift
struct WrappedLabel: UIViewRepresentable {
let text: String
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
return label
}
func updateUIView(_ label: UILabel, context: Context) {
label.text = text
}
// 自定义尺寸适配——SwiftUI会在布局时调用此方法
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UILabel, context: Context) -> CGSize? {
let width = proposal.width ?? UIView.layoutFittingCompressedSize.width
return uiView.systemLayoutSizeFitting(
CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
}
}Scroll-Tracking Navigation Bars (iOS 15+)
导航栏滚动跟踪(iOS 15+)
When wrapping a UIScrollView subclass, tell the navigation bar which scroll view to track for large title collapse:
swift
func makeUIView(context: Context) -> UITableView {
let table = UITableView()
return table
}
func updateUIView(_ table: UITableView, context: Context) {
// Tell the nearest navigation controller to track this scroll view
// for inline/large title transitions
if let navController = sequence(first: table as UIResponder, next: \.next)
.compactMap({ $0 as? UINavigationController }).first {
navController.navigationBar.setContentScrollView(table, forEdge: .top)
}
}Without this, navigation bar large titles won't collapse when scrolling a wrapped UIScrollView.
当封装UIScrollView子类时,需告知导航栏跟踪哪个滚动视图以实现大标题折叠效果:
swift
func makeUIView(context: Context) -> UITableView {
let table = UITableView()
return table
}
func updateUIView(_ table: UITableView, context: Context) {
// 告知最近的导航控制器跟踪此滚动视图
// 以实现内嵌/大标题的过渡效果
if let navController = sequence(first: table as UIResponder, next: \.next)
.compactMap({ $0 as? UINavigationController }).first {
navController.navigationBar.setContentScrollView(table, forEdge: .top)
}
}如果不设置此属性,当滚动封装的UIScrollView时,导航栏的大标题将不会折叠。
Animation Bridging
动画桥接
Use to bridge SwiftUI animations into UIKit:
context.transaction.animationswift
func updateUIView(_ uiView: UIView, context: Context) {
if context.transaction.animation != nil {
UIView.animate(withDuration: 0.3) {
uiView.alpha = isVisible ? 1 : 0
}
} else {
uiView.alpha = isVisible ? 1 : 0
}
}iOS 18+ animation unification: SwiftUI animations can be applied directly to UIKit views via . However, be aware of incompatibilities:
UIView.animate(_:)- SwiftUI animations are NOT backed by CAAnimation — they use a different rendering path
- Incompatible with and
UIViewPropertyAnimatorkeyframe animationsUIView - Velocity retargeting: Re-targeted SwiftUI animations carry forward velocity from interrupted animations, creating fluid transitions
For comprehensive animation bridging patterns, see Part 10.
/skill axiom-swiftui-animation-ref使用将SwiftUI动画桥接到UIKit:
context.transaction.animationswift
func updateUIView(_ uiView: UIView, context: Context) {
if context.transaction.animation != nil {
UIView.animate(withDuration: 0.3) {
uiView.alpha = isVisible ? 1 : 0
}
} else {
uiView.alpha = isVisible ? 1 : 0
}
}iOS 18+ 动画统一:SwiftUI动画可通过直接应用于UIKit视图。但需注意兼容性问题:
UIView.animate(_:)- SwiftUI动画并非基于CAAnimation——它们使用不同的渲染路径
- 不兼容和
UIViewPropertyAnimator关键帧动画UIView - 速度重定向:重定向后的SwiftUI动画会继承被中断动画的速度,实现流畅过渡
如需了解完整的动画桥接模式,请查看第10部分。
/skill axiom-swiftui-animation-refPart 2: UIViewControllerRepresentable — Wrapping UIViewControllers
第2部分:UIViewControllerRepresentable — 封装UIViewControllers
Use when wrapping a full — pickers, mail compose, Safari, camera, or any controller that manages its own view hierarchy.
UIViewController当需要封装完整的时使用此方案——例如选择器、邮件撰写、Safari、相机,或任何管理自身视图层级的控制器。
UIViewControllerLifecycle
生命周期
makeUIViewController(context:) → Called ONCE. Create and configure.
updateUIViewController(_:context:) → Called on SwiftUI state changes.
dismantleUIViewController(_:coordinator:) → Cleanup.makeUIViewController(context:) → 仅调用一次。用于创建和配置控制器。
updateUIViewController(_:context:) → SwiftUI状态变更时调用。
dismantleUIViewController(_:coordinator:) → 清理资源。Canonical Example: PHPickerViewController
典型示例:PHPickerViewController
swift
struct PhotoPicker: UIViewControllerRepresentable {
@Binding var selectedImages: [UIImage]
@Environment(\.dismiss) private var dismiss
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.selectionLimit = 5
config.filter = .images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ picker: PHPickerViewController, context: Context) {
// PHPicker doesn't support updates after creation
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, PHPickerViewControllerDelegate {
var parent: PhotoPicker
init(_ parent: PhotoPicker) { self.parent = parent }
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.selectedImages = []
for result in results {
result.itemProvider.loadObject(ofClass: UIImage.self) { image, _ in
if let image = image as? UIImage {
DispatchQueue.main.async {
self.parent.selectedImages.append(image)
}
}
}
}
parent.dismiss()
}
}
}swift
struct PhotoPicker: UIViewControllerRepresentable {
@Binding var selectedImages: [UIImage]
@Environment(\.dismiss) private var dismiss
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.selectionLimit = 5
config.filter = .images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ picker: PHPickerViewController, context: Context) {
// PHPicker创建后不支持更新
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, PHPickerViewControllerDelegate {
var parent: PhotoPicker
init(_ parent: PhotoPicker) { self.parent = parent }
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.selectedImages = []
for result in results {
result.itemProvider.loadObject(ofClass: UIImage.self) { image, _ in
if let image = image as? UIImage {
DispatchQueue.main.async {
self.parent.selectedImages.append(image)
}
}
}
}
parent.dismiss()
}
}
}When the Controller Presents Its Own UI
当控制器自行展示UI时
Some controllers (UIImagePickerController, MFMailComposeViewController, SFSafariViewController) present their own full-screen UI. Handle dismissal through the coordinator:
swift
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
var parent: MailComposer
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult, error: Error?) {
parent.dismiss() // Let SwiftUI handle the dismissal
}
}Don't call directly from the coordinator — let SwiftUI's or the binding that controls presentation handle it.
controller.dismiss(animated:)@Environment(\.dismiss)部分控制器(如UIImagePickerController、MFMailComposeViewController、SFSafariViewController)会自行展示全屏UI。需通过协调器处理关闭逻辑:
swift
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
var parent: MailComposer
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult, error: Error?) {
parent.dismiss() // 让SwiftUI处理关闭逻辑
}
}不要直接在协调器中调用——让SwiftUI的或控制展示状态的来处理。
controller.dismiss(animated:)@Environment(\.dismiss)@BindingPresentation Context
展示上下文
The wrapped controller doesn't automatically inherit SwiftUI's navigation context. If you need the controller to push onto a navigation stack, you need UIViewControllerRepresentable inside a NavigationStack, and the controller needs access to the navigation controller:
swift
// ❌ This won't push — the controller has no navigationController
struct WrappedVC: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MyViewController {
let vc = MyViewController()
vc.navigationController?.pushViewController(otherVC, animated: true) // nil
return vc
}
}
// ✅ Present modally instead, or use UIHostingController in a UIKit navigation flow
.sheet(isPresented: $showPicker) {
PhotoPicker(selectedImages: $images)
}封装后的控制器不会自动继承SwiftUI的导航上下文。如果需要将控制器推入导航栈,需将UIViewControllerRepresentable放在NavigationStack中,且控制器需能访问导航控制器:
swift
// ❌ 无法推入——控制器没有navigationController
struct WrappedVC: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MyViewController {
let vc = MyViewController()
vc.navigationController?.pushViewController(otherVC, animated: true) // nil
return vc
}
}
// ✅ 改为模态展示,或在UIKit导航流程中使用UIHostingController
.sheet(isPresented: $showPicker) {
PhotoPicker(selectedImages: $images)
}Part 2b: UIGestureRecognizerRepresentable (iOS 18+)
第2b部分:UIGestureRecognizerRepresentable(iOS 18+)
Use when you need a UIKit gesture recognizer in SwiftUI — for gestures that SwiftUI's native gesture API doesn't support (custom subclasses, precise UIKit gesture state machine, hit testing control).
Pre-iOS 18 fallback: Attach the gesture recognizer to a transparent wrapped with , using the Coordinator as the target/action receiver (see Part 1 Coordinator Pattern). You lose but can use the recognizer's directly.
UIViewUIViewRepresentableCoordinateSpaceConverterlocation(in:)当需要在SwiftUI中使用UIKit手势识别器时使用此方案——适用于SwiftUI原生手势API不支持的场景(如自定义子类、精确的UIKit手势状态机、点击测试控制)。
iOS 18之前的兼容方案:将手势识别器附加到一个用封装的透明上,使用协调器作为目标/动作接收器(参见第1部分的协调器模式)。这种方式无法使用,但可以直接使用识别器的方法。
UIViewRepresentableUIViewCoordinateSpaceConverterlocation(in:)Lifecycle
生命周期
makeUIGestureRecognizer(context:) → Called ONCE. Create the recognizer.
handleUIGestureRecognizerAction(_:context:) → Called when the gesture is recognized.
updateUIGestureRecognizer(_:context:) → Called on SwiftUI state changes.
makeCoordinator(converter:) → Optional. Create coordinator for state.No manual target/action — the system manages action target installation. Implement instead.
handleUIGestureRecognizerActionmakeUIGestureRecognizer(context:) → 仅调用一次。用于创建识别器。
handleUIGestureRecognizerAction(_:context:) → 手势被识别时调用。
updateUIGestureRecognizer(_:context:) → SwiftUI状态变更时调用。
makeCoordinator(converter:) → 可选。用于创建状态协调器。无需手动设置目标/动作——系统会管理动作目标的安装。只需实现方法即可。
handleUIGestureRecognizerActionCanonical Example: Long Press with Location
典型示例:带位置信息的长按手势
swift
struct LongPressGesture: UIGestureRecognizerRepresentable {
@Binding var pressLocation: CGPoint?
func makeUIGestureRecognizer(context: Context) -> UILongPressGestureRecognizer {
let recognizer = UILongPressGestureRecognizer()
recognizer.minimumPressDuration = 0.5
return recognizer
}
func handleUIGestureRecognizerAction(
_ recognizer: UILongPressGestureRecognizer, context: Context
) {
switch recognizer.state {
case .began:
// localLocation converts UIKit coordinates to SwiftUI coordinate space
pressLocation = context.converter.localLocation
case .ended, .cancelled:
pressLocation = nil
default:
break
}
}
}
// Usage
struct ContentView: View {
@State private var pressLocation: CGPoint?
var body: some View {
Rectangle()
.gesture(LongPressGesture(pressLocation: $pressLocation))
}
}swift
struct LongPressGesture: UIGestureRecognizerRepresentable {
@Binding var pressLocation: CGPoint?
func makeUIGestureRecognizer(context: Context) -> UILongPressGestureRecognizer {
let recognizer = UILongPressGestureRecognizer()
recognizer.minimumPressDuration = 0.5
return recognizer
}
func handleUIGestureRecognizerAction(
_ recognizer: UILongPressGestureRecognizer, context: Context
) {
switch recognizer.state {
case .began:
// localLocation将UIKit坐标转换为SwiftUI坐标空间
pressLocation = context.converter.localLocation
case .ended, .cancelled:
pressLocation = nil
default:
break
}
}
}
// 使用示例
struct ContentView: View {
@State private var pressLocation: CGPoint?
var body: some View {
Rectangle()
.gesture(LongPressGesture(pressLocation: $pressLocation))
}
}CoordinateSpaceConverter
CoordinateSpaceConverter
The bridges UIKit gesture coordinates into SwiftUI coordinate spaces:
context.converter| Property/Method | Description |
|---|---|
| Gesture position in the attached SwiftUI view's space |
| Gesture movement in local space |
| Gesture velocity in local space |
| Transform location to an ancestor coordinate space |
| Transform translation to an ancestor space |
| Transform velocity to an ancestor space |
context.converter| 属性/方法 | 描述 |
|---|---|
| 手势在附加的SwiftUI视图空间中的位置 |
| 手势在本地空间中的移动距离 |
| 手势在本地空间中的速度 |
| 将位置转换为祖先视图的坐标空间 |
| 将移动距离转换为祖先视图的坐标空间 |
| 将速度转换为祖先视图的坐标空间 |
When to Use This vs SwiftUI Gestures
何时使用此方案 vs SwiftUI原生手势
| Need | Use |
|---|---|
| Standard tap, drag, long press, rotation, magnification | SwiftUI native gestures |
Custom | |
Precise control over gesture state machine ( | |
Gesture that requires | |
| Coordinate space conversion between UIKit and SwiftUI | |
| 需求 | 方案选择 |
|---|---|
| 标准点击、拖拽、长按、旋转、缩放 | SwiftUI原生手势 |
自定义 | |
对手势状态机( | |
需要 | 带Coordinator的 |
| UIKit与SwiftUI之间的坐标空间转换 | |
Part 3: UIHostingController — SwiftUI Inside UIKit
第3部分:UIHostingController — 在UIKit中嵌入SwiftUI
Use when embedding SwiftUI views in an existing UIKit navigation hierarchy.
当需要在现有UIKit导航层级中嵌入SwiftUI视图时使用此方案。
Basic Embedding
基础嵌入方式
swift
// Push onto UIKit navigation stack
let profileView = ProfileView(user: user)
let hostingController = UIHostingController(rootView: profileView)
navigationController?.pushViewController(hostingController, animated: true)
// Present modally
let settingsView = SettingsView()
let hostingController = UIHostingController(rootView: settingsView)
hostingController.modalPresentationStyle = .pageSheet
present(hostingController, animated: true)swift
// 推入UIKit导航栈
let profileView = ProfileView(user: user)
let hostingController = UIHostingController(rootView: profileView)
navigationController?.pushViewController(hostingController, animated: true)
// 模态展示
let settingsView = SettingsView()
let hostingController = UIHostingController(rootView: settingsView)
hostingController.modalPresentationStyle = .pageSheet
present(hostingController, animated: true)Child View Controller Embedding
子视图控制器嵌入
When embedding as a child VC (e.g., a SwiftUI card inside a UIKit layout):
swift
let swiftUIView = StatusCard(status: currentStatus)
let hostingController = UIHostingController(rootView: swiftUIView)
hostingController.sizingOptions = .intrinsicContentSize // iOS 16+
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: headerView.bottomAnchor)
])
hostingController.didMove(toParent: self)sizingOptions: .intrinsicContentSizesizingOptionsOptionSet- — auto-invalidates intrinsic content size when SwiftUI content changes
.intrinsicContentSize - — tracks content's ideal size in the controller's
.preferredContentSizepreferredContentSize
当作为子视图控制器嵌入时(例如在UIKit布局中嵌入SwiftUI卡片):
swift
let swiftUIView = StatusCard(status: currentStatus)
let hostingController = UIHostingController(rootView: swiftUIView)
hostingController.sizingOptions = .intrinsicContentSize // iOS 16+
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: headerView.bottomAnchor)
])
hostingController.didMove(toParent: self)sizingOptions: .intrinsicContentSizesizingOptionsOptionSet- —— 当SwiftUI内容变化时自动失效固有内容尺寸
.intrinsicContentSize - —— 在控制器的
.preferredContentSize中跟踪内容的理想尺寸preferredContentSize
Explicit Size Queries
显式尺寸查询
Use to calculate the SwiftUI content's preferred size for Auto Layout integration:
sizeThatFits(in:)swift
let hostingController = UIHostingController(rootView: CompactCard(item: item))
// Query preferred size for a given width constraint
let fittingSize = hostingController.sizeThatFits(in: CGSize(width: 320, height: .infinity))
// Returns the optimal CGSize for the SwiftUI contentThis is useful when you need the hosting controller's size before adding it to the view hierarchy, or when embedding in contexts where alone isn't sufficient (e.g., manually sizing popover content).
sizingOptions使用计算SwiftUI内容的首选尺寸,以集成到Auto Layout中:
sizeThatFits(in:)swift
let hostingController = UIHostingController(rootView: CompactCard(item: item))
// 查询给定宽度约束下的首选尺寸
let fittingSize = hostingController.sizeThatFits(in: CGSize(width: 320, height: .infinity))
// 返回SwiftUI内容的最优CGSize当需要在将宿主控制器添加到视图层级之前获取其尺寸,或在 alone不足以满足的场景中嵌入时(例如手动调整弹出框内容尺寸),此方法非常有用。
sizeThatFitsEnvironment Bridging
环境桥接
Standard system environment values (, , ) bridge automatically through the UIKit trait system. Custom keys from a parent SwiftUI view do NOT — unless you use .
colorSchemesizeCategorylocale@EnvironmentUITraitBridgedEnvironmentKeyOption 1: Inject explicitly (simplest, works on all versions):
swift
let view = DetailView(store: appStore, theme: currentTheme)
let hostingController = UIHostingController(rootView: view)Option 2: UITraitBridgedEnvironmentKey (iOS 17+, bidirectional bridging):
Bridge custom environment values between UIKit traits and SwiftUI environment:
swift
// 1. Define a UIKit trait
struct FeatureOneTrait: UITraitDefinition {
static let defaultValue = false
}
extension UIMutableTraits {
var featureOne: Bool {
get { self[FeatureOneTrait.self] }
set { self[FeatureOneTrait.self] = newValue }
}
}
// 2. Define a SwiftUI EnvironmentKey
struct FeatureOneKey: EnvironmentKey {
static let defaultValue = false
}
extension EnvironmentValues {
var featureOne: Bool {
get { self[FeatureOneKey.self] }
set { self[FeatureOneKey.self] = newValue }
}
}
// 3. Bridge them
extension FeatureOneKey: UITraitBridgedEnvironmentKey {
static func read(from traitCollection: UITraitCollection) -> Bool {
traitCollection[FeatureOneTrait.self]
}
static func write(to mutableTraits: inout UIMutableTraits, value: Bool) {
mutableTraits.featureOne = value
}
}Now automatically syncs in both directions — UIKit update SwiftUI views, and SwiftUI updates UIKit views.
@Environment(\.featureOne)traitOverrides.environment(\.featureOne, true)To push values from UIKit into hosted SwiftUI content:
swift
// In any UIKit view controller — flows down to UIHostingController children
viewController.traitOverrides.featureOne = true标准系统环境值(、、)会通过UIKit trait系统自动桥接。但父SwiftUI视图的自定义键不会自动桥接——除非使用。
colorSchemesizeCategorylocale@EnvironmentUITraitBridgedEnvironmentKey方案1:显式注入(最简单,支持所有版本):
swift
let view = DetailView(store: appStore, theme: currentTheme)
let hostingController = UIHostingController(rootView: view)方案2:UITraitBridgedEnvironmentKey(iOS 17+,双向桥接):
将自定义环境值在UIKit trait与SwiftUI环境之间桥接:
swift
// 1. 定义UIKit trait
struct FeatureOneTrait: UITraitDefinition {
static let defaultValue = false
}
extension UIMutableTraits {
var featureOne: Bool {
get { self[FeatureOneTrait.self] }
set { self[FeatureOneTrait.self] = newValue }
}
}
// 2. 定义SwiftUI EnvironmentKey
struct FeatureOneKey: EnvironmentKey {
static let defaultValue = false
}
extension EnvironmentValues {
var featureOne: Bool {
get { self[FeatureOneKey.self] }
set { self[FeatureOneKey.self] = newValue }
}
}
// 3. 桥接两者
extension FeatureOneKey: UITraitBridgedEnvironmentKey {
static func read(from traitCollection: UITraitCollection) -> Bool {
traitCollection[FeatureOneTrait.self]
}
static func write(to mutableTraits: inout UIMutableTraits, value: Bool) {
mutableTraits.featureOne = value
}
}现在会自动双向同步——UIKit的会更新SwiftUI视图,SwiftUI的也会更新UIKit视图。
@Environment(\.featureOne)traitOverrides.environment(\.featureOne, true)要将值从UIKit推送到宿主SwiftUI内容中:
swift
// 在任意UIKit视图控制器中——会传递给UIHostingController子视图
viewController.traitOverrides.featureOne = trueUIHostingConfiguration (iOS 16+)
UIHostingConfiguration(iOS 16+)
Use SwiftUI views as UICollectionView or UITableView cells:
swift
cell.contentConfiguration = UIHostingConfiguration {
HStack {
Image(systemName: item.icon)
.foregroundStyle(.tint)
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
.margins(.all, EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.minSize(width: nil, height: 44) // Minimum tap target height
.background(.quaternarySystemFill) // ShapeStyle backgroundCell clipping? UIHostingConfiguration cells self-size. If cells are clipped, the collection view layout likely uses fixed — switch to dimensions in your compositional layout so cells can grow to fit the SwiftUI content.
itemSizeestimated将SwiftUI视图用作UICollectionView或UITableView的单元格:
swift
cell.contentConfiguration = UIHostingConfiguration {
HStack {
Image(systemName: item.icon)
.foregroundStyle(.tint)
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
.margins(.all, EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.minSize(width: nil, height: 44) // 最小点击目标高度
.background(.quaternarySystemFill) // 背景样式单元格被裁剪? UIHostingConfiguration单元格会自动适配尺寸。如果单元格被裁剪,可能是因为集合视图布局使用了固定的——在组合布局中切换为尺寸,让单元格可以根据SwiftUI内容自动调整大小。
itemSizeestimatedAdvantages over full UIHostingController
相较于完整UIHostingController的优势
- No child view controller management
- Automatic cell sizing
- Self-sizing invalidation on state change
- Compatible with diffable data sources
- 无需管理子视图控制器
- 自动单元格尺寸适配
- 状态变化时自动失效尺寸
- 兼容可差分数据源
When to use UIHostingConfiguration vs UIHostingController
何时使用UIHostingConfiguration vs UIHostingController
| Scenario | Use |
|---|---|
| Cell content in UICollectionView/UITableView | UIHostingConfiguration |
| Full screen or navigation destination | UIHostingController |
| Child VC in a layout | UIHostingController |
| Overlay or decoration | UIHostingConfiguration in a supplementary view |
| 场景 | 方案选择 |
|---|---|
| UICollectionView/UITableView中的单元格内容 | UIHostingConfiguration |
| 全屏或导航目标页 | UIHostingController |
| 布局中的子视图控制器 | UIHostingController |
| 覆盖层或装饰视图 | 补充视图中的UIHostingConfiguration |
Scroll-Tracking for Navigation Bars
导航栏滚动跟踪
When a UIHostingController contains a scroll view and is pushed onto a UINavigationController, large title collapse may not work. Use :
setContentScrollViewswift
let hostingController = UIHostingController(rootView: ScrollableListView())
// After pushing, tell the nav bar to track the scroll view
if let scrollView = hostingController.view.subviews.compactMap({ $0 as? UIScrollView }).first {
navigationController?.navigationBar.setContentScrollView(scrollView, forEdge: .top)
}This is a common issue when embedding SwiftUI or in UIKit navigation.
ListScrollView当UIHostingController包含滚动视图并被推入UINavigationController时,大标题折叠可能无法正常工作。需使用:
setContentScrollViewswift
let hostingController = UIHostingController(rootView: ScrollableListView())
// 推入后,告知导航栏跟踪该滚动视图
if let scrollView = hostingController.view.subviews.compactMap({ $0 as? UIScrollView }).first {
navigationController?.navigationBar.setContentScrollView(scrollView, forEdge: .top)
}这是在UIKit导航中嵌入SwiftUI 或时的常见问题。
ListScrollViewKeyboard Handling in Hybrid Layouts
混合布局中的键盘处理
When mixing UIKit and SwiftUI, keyboard avoidance may not work automatically. Use (iOS 15+) for constraint-based keyboard tracking in UIKit layouts that contain SwiftUI content:
UIKeyboardLayoutGuideswift
// Constrain the hosting controller's view above the keyboard
hostingController.view.bottomAnchor.constraint(
equalTo: view.keyboardLayoutGuide.topAnchor
).isActive = true当混合使用UIKit和SwiftUI时,键盘自动避让可能无法正常工作。在包含SwiftUI内容的UIKit布局中,使用(iOS 15+)实现基于约束的键盘跟踪:
UIKeyboardLayoutGuideswift
// 将宿主控制器的视图约束在键盘上方
hostingController.view.bottomAnchor.constraint(
equalTo: view.keyboardLayoutGuide.topAnchor
).isActive = truePart 4: Shared State with @Observable
第4部分:使用@Observable共享状态
When UIKit and SwiftUI coexist in the same app, you need a shared model layer. (iOS 17+) works naturally in both frameworks without Combine.
@Observable当UIKit和SwiftUI在同一应用中共存时,需要一个共享模型层。(iOS 17+)可在两个框架中自然工作,无需Combine。
@Observable@Observable as the Shared Model Layer
@Observable作为共享模型层
swift
@Observable
class AppState {
var userName: String = ""
var isLoggedIn: Bool = false
var itemCount: Int = 0
}SwiftUI side — standard property wrappers:
swift
struct ProfileView: View {
@State var appState: AppState // or @Environment, @Bindable
var body: some View {
Text("Welcome, \(appState.userName)")
Text("\(appState.itemCount) items")
}
}Why UIKit needs explicit observation: SwiftUI's rendering engine automatically participates in the Observation framework — when a view's accesses an property, SwiftUI registers that access and re-renders when it changes. UIKit is imperative and has no equivalent re-evaluation mechanism, so you must opt in explicitly.
body@ObservableUIKit side (pre-iOS 26) — manual observation with :
withObservationTracking()swift
class DashboardViewController: UIViewController {
let appState: AppState
override func viewDidLoad() {
super.viewDidLoad()
observeState()
}
private func observeState() {
withObservationTracking {
// Properties accessed here are tracked
titleLabel.text = appState.userName
countLabel.text = "\(appState.itemCount) items"
} onChange: {
// Fires ONCE on the thread that mutated the property — must re-register
// Always dispatch to main: onChange can fire on ANY thread
DispatchQueue.main.async { [weak self] in
self?.observeState()
}
}
}
}UIKit side (iOS 26+) — automatic observation tracking:
UIKit automatically tracks property access in designated lifecycle methods. Properties read in these methods trigger automatic UI updates when they change:
@Observable| Method | Class | What it updates |
|---|---|---|
| UIView, UIViewController | Content and styling |
| UIView | Geometry and positioning |
| UIViewController | Pre-layout |
| UIView | Custom drawing |
swift
class DashboardViewController: UIViewController {
let appState: AppState
// iOS 26+: Properties accessed here are auto-tracked
override func updateProperties() {
super.updateProperties()
titleLabel.text = appState.userName
countLabel.text = "\(appState.itemCount) items"
}
}Info.plist requirement: In iOS 18, add to your Info.plist to enable automatic observation tracking. iOS 26+ enables it by default.
UIObservationTrackingEnabled = trueswift
@Observable
class AppState {
var userName: String = ""
var isLoggedIn: Bool = false
var itemCount: Int = 0
}SwiftUI端 —— 使用标准属性包装器:
swift
struct ProfileView: View {
@State var appState: AppState // 或@Environment、@Bindable
var body: some View {
Text("Welcome, \(appState.userName)")
Text("\(appState.itemCount) items")
}
}为什么UIKit需要显式观察? SwiftUI的渲染引擎会自动参与Observation框架——当视图的访问属性时,SwiftUI会注册该访问,并在属性变化时重新渲染视图。而UIKit是命令式的,没有类似的自动重评估机制,因此必须手动开启观察。
body@ObservableUIKit端(iOS 26之前) —— 使用手动观察:
withObservationTracking()swift
class DashboardViewController: UIViewController {
let appState: AppState
override func viewDidLoad() {
super.viewDidLoad()
observeState()
}
private func observeState() {
withObservationTracking {
// 此处访问的属性会被跟踪
titleLabel.text = appState.userName
countLabel.text = "\(appState.itemCount) items"
} onChange: {
// 属性变更时触发一次——必须重新注册
// 始终调度到主线程:onChange可能在任意线程触发
DispatchQueue.main.async { [weak self] in
self?.observeState()
}
}
}
}UIKit端(iOS 26+) —— 自动观察跟踪:
UIKit会在指定的生命周期方法中自动跟踪属性的访问。在这些方法中读取的属性会在变化时自动触发UI更新:
@Observable| 方法 | 类 | 更新内容 |
|---|---|---|
| UIView、UIViewController | 内容和样式 |
| UIView | 几何布局和位置 |
| UIViewController | 布局前准备 |
| UIView | 自定义绘图 |
swift
class DashboardViewController: UIViewController {
let appState: AppState
// iOS 26+:此处访问的属性会被自动跟踪
override func updateProperties() {
super.updateProperties()
titleLabel.text = appState.userName
countLabel.text = "\(appState.itemCount) items"
}
}Info.plist要求:在iOS 18中,需在Info.plist中添加以启用自动观察跟踪。iOS 26+默认启用该功能。
UIObservationTrackingEnabled = trueiOS 16 Fallback: ObservableObject + Combine
iOS 16兼容方案:ObservableObject + Combine
If targeting iOS 16 (before ), use with and observe via Combine on the UIKit side:
@ObservableObservableObject@Publishedswift
class AppState: ObservableObject {
@Published var userName: String = ""
@Published var itemCount: Int = 0
}
// UIKit side — observe with Combine sink
class DashboardViewController: UIViewController {
let appState: AppState
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
appState.$userName
.receive(on: DispatchQueue.main)
.sink { [weak self] name in
self?.titleLabel.text = name
}
.store(in: &cancellables)
}
}如果需要兼容iOS 16(之前的版本),可使用配合,并在UIKit端通过Combine观察:
@ObservableObservableObject@Publishedswift
class AppState: ObservableObject {
@Published var userName: String = ""
@Published var itemCount: Int = 0
}
// UIKit端——通过Combine sink观察
class DashboardViewController: UIViewController {
let appState: AppState
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
appState.$userName
.receive(on: DispatchQueue.main)
.sink { [weak self] name in
self?.titleLabel.text = name
}
.store(in: &cancellables)
}
}Migration Note
迁移说明
@ObservableObservableObject@Published- Replace classes with
ObservableObject@Observable - Remove property wrappers (observation is automatic)
@Published - SwiftUI views keep working — and
@Statesupport@Environmentdirectly@Observable - UIKit views gain observation through (iOS 17+) or automatic tracking (iOS 26+)
withObservationTracking()
@ObservableObservableObject@Published- 将类替换为
ObservableObject@Observable - 移除属性包装器(观察是自动的)
@Published - SwiftUI视图可直接使用——和
@State原生支持@Environment@Observable - UIKit视图可通过(iOS 17+)或自动跟踪(iOS 26+)实现观察
withObservationTracking()
Part 5: Common Gotchas
第5部分:常见陷阱
| Gotcha | Symptom | Fix |
|---|---|---|
| Coordinator retains parent | Memory leak, views never deallocate | Coordinator stores |
| updateUIView called excessively | UIKit view flickers, resets scroll position, drops user input | Guard with equality checks. Compare old vs new values before applying changes. |
| Environment doesn't cross bridge | Custom environment values are nil/default | Use |
| Large title won't collapse | Navigation bar stays expanded when scrolling wrapped UIScrollView | Call |
| UIHostingController sizing wrong | View is zero-sized or jumps after layout | Use |
| Mixed navigation stacks | Unpredictable back button behavior, lost state | Don't mix UINavigationController and NavigationStack in the same flow. Migrate entire navigation subtrees. |
| makeUIView called multiple times | View recreated unexpectedly | Ensure the |
| Coordinator not receiving callbacks | Delegate methods never fire | Set |
| Layout properties modified on representable view | View jumps, disappears, or has inconsistent layout | Never modify |
| Keyboard hides content in hybrid layout | Text field or content hidden behind keyboard | Use |
| @Observable not updating UIKit views | UIKit views show stale data after model changes | Use |
| 陷阱 | 症状 | 修复方案 |
|---|---|---|
| Coordinator持有父结构体的强引用 | memory泄漏,视图永远不会被释放 | Coordinator存储 |
| updateUIView被过度调用 | UIKit视图闪烁,滚动位置重置,丢失用户输入 | 使用相等性检查做防护。在应用变更前比较新旧值。 |
| 环境无法跨桥接层传递 | 自定义环境值为nil/默认值 | 使用 |
| 大标题无法折叠 | 滚动封装的UIScrollView时,导航栏保持展开状态 | 调用导航栏的 |
| UIHostingController尺寸异常 | 视图尺寸为0或布局后跳动 | 使用 |
| 混合导航栈 | 返回按钮行为不可预测,状态丢失 | 不要在同一流程中混合UINavigationController和NavigationStack。迁移整个导航子树。 |
| makeUIView被多次调用 | 视图被意外重建 | 确保 |
| Coordinator未收到回调 | 代理方法永远不会触发 | 在 |
| 修改可表示视图的布局属性 | 视图跳动、消失或布局不一致 | 永远不要修改封装后UIView的 |
| 混合布局中键盘遮挡内容 | 文本框或内容被键盘遮挡 | 在UIKit中使用 |
| @Observable未更新UIKit视图 | 模型变化后UIKit视图显示陈旧数据 | 使用 |
Part 6: Anti-Patterns
第6部分:反模式
| Pattern | Problem | Fix |
|---|---|---|
| "I'll use UIViewRepresentable for the whole screen" | UIViewControllerRepresentable exists for controllers that manage their own view hierarchy, handle rotation, and participate in the responder chain | Use UIViewControllerRepresentable for UIViewControllers. UIViewRepresentable is for bare UIViews. |
| "I don't need a coordinator, I'll use closures" | Closures capture the struct value (not reference), become stale on updates, and can't conform to delegate protocols | Use the Coordinator. It's a stable reference type that SwiftUI keeps alive and updates. |
| "I'll rebuild the UIKit view every update" | | Create in |
| "SwiftUI environment will just work across the bridge" | Custom | Use |
| "I'll dismiss the UIKit controller directly" | Calling | Use |
| "I'll skip dismantleUIView, it'll clean up automatically" | Timers, observers, and KVO registrations on the UIView leak | Implement |
| 模式 | 问题 | 修复方案 |
|---|---|---|
| "我要给整个屏幕用UIViewRepresentable" | UIViewControllerRepresentable专为管理自身视图层级、处理旋转、参与响应链的控制器设计 | 对UIViewControllers使用UIViewControllerRepresentable。UIViewRepresentable仅适用于独立UIView。 |
| "我不需要Coordinator,用闭包就行" | 闭包捕获结构体值(而非引用),更新后会失效,且不支持代理协议 | 使用Coordinator。它是由SwiftUI管理的稳定引用类型。 |
| "我要在每次update时重建UIKit视图" | | 在 |
| "SwiftUI环境会自动跨桥接层生效" | 自定义 | 使用 |
| "我要直接关闭UIKit控制器" | 在Coordinator中调用 | 使用 |
| "我跳过dismantleUIView,系统会自动清理" | UIView上的计时器、观察者和KVO注册会泄漏 | 实现 |
Resources
资源
WWDC: 2019-231, 2022-10072, 2023-10149, 2024-10118, 2024-10145, 2025-243, 2025-256
Docs: /swiftui/uiviewrepresentable, /swiftui/uiviewcontrollerrepresentable, /swiftui/uigesturerecognizerrepresentable, /uikit/uihostingcontroller, /uikit/uihostingconfiguration, /swiftui/uitraitbridgedenvironmentkey, /observation, /uikit/updating-views-automatically-with-observation-tracking
Skills: app-composition, swiftui-animation-ref, camera-capture, transferable-ref, swift-concurrency
WWDC:2019-231, 2022-10072, 2023-10149, 2024-10118, 2024-10145, 2025-243, 2025-256
文档:/swiftui/uiviewrepresentable, /swiftui/uiviewcontrollerrepresentable, /swiftui/uigesturerecognizerrepresentable, /uikit/uihostingcontroller, /uikit/uihostingconfiguration, /swiftui/uitraitbridgedenvironmentkey, /observation, /uikit/updating-views-automatically-with-observation-tracking
相关技能:app-composition, swiftui-animation-ref, camera-capture, transferable-ref, swift-concurrency