UIKit 互操作与 Hosting
多篇文章(如 SwiftUI Under the Hood)共同结论:iOS 上 SwiftUI 最终仍通过 UIKit 渲染。Text → UILabel 类实现;List → UITableView 风格基础设施。桥梁核心是 UIHostingController<Content>。
UIHostingController 放在哪
纯 SwiftUI App
@main struct App → WindowGroup 内部已是 hosting 树,无需手写。
UIKit 宿主(渐进迁移)
swift
let root = UIHostingController(rootView: MainTabView())
root.view.backgroundColor = .systemBackground
window.rootViewController = root注意:
- 安全区、方向、状态栏由 hosting VC 传递
- 嵌入
UINavigationController时,SwiftUI 内再用NavigationStack可能 双导航栏 — 通常二选一
局部 SwiftUI
swift
final class LegacyVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let host = UIHostingController(rootView: SettingsPanel())
addChild(host)
view.addSubview(host.view)
host.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
host.view.topAnchor.constraint(equalTo: view.topAnchor),
host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
host.didMove(toParent: self)
}
}host.view 参与 Auto Layout;SwiftUI 内部布局仍走提案模型。
UIViewRepresentable 模板
swift
struct MapTile: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
return map
}
func updateUIView(_ map: MKMapView, context: Context) {
// 轻量同步;避免每次 body 都开新 region 动画
let region = MKCoordinateRegion(center: coordinate, span: .init(latitudeDelta: 0.05, longitudeDelta: 0.05))
if map.region.center.latitude != coordinate.latitude {
map.setRegion(region, animated: context.transaction.animation != nil)
}
}
func makeCoordinator() -> Coordinator { Coordinator() }
final class Coordinator: NSObject, MKMapViewDelegate {}
}原则:
| 方法 | 应做 |
|---|---|
makeUIView | 一次性创建、设 delegate |
updateUIView | 把 SwiftUI 状态 → UIKit 属性,要幂等 |
makeCoordinator | 委托、target-action、KVO |
反模式:在 updateUIView 里 addSubview 重复叠加。
更新频率陷阱
body 每帧重算 → updateUIView 狂调。
用 Equatable 包装减少 diff:
swift
struct MapTile: UIViewRepresentable, Equatable {
var coordinate: CLLocationCoordinate2D
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.coordinate.latitude == rhs.coordinate.latitude &&
lhs.coordinate.longitude == rhs.coordinate.longitude
}
// ...
}
// 使用处
MapTile(coordinate: coord).equatable()或使用 UIViewControllerRepresentable 处理复杂 VC 生命周期。
尺寸:iOS 16+ sizeThatFits
swift
func sizeThatFits(_ proposal: ProposedViewSize, uiView: MKMapView, context: Context) -> CGSize? {
CGSize(width: proposal.width ?? 320, height: proposal.height ?? 240)
}否则 SwiftUI 可能给 UIKit 视图 零高度 或撑满错误轴向。
事件与 Focus
- UIKit → SwiftUI:Coordinator 里
Binding或@ObservableStore - SwiftUI → UIKit:
updateUIView传参 - 键盘:注意
UIViewRepresentable与ScrollView抢 responder
SwiftUI in Cell
List 中嵌 heavy UIViewRepresentable 仍贵。可选:
- 纯 SwiftUI 重写
UICollectionView+ hosting configuration(iOS 16UIHostingConfiguration)- 预渲染静态图 + 点击再加载地图
延伸阅读
visionOS / macOS 桥接类型名不同,以平台文档为准。