2、UIHostingController入门:在UIKit中嵌入第一个SwiftUI视图、生命周期管理、布局适配
2.1 为什么需要UIHostingController?
说实话,SwiftUI刚出来那会儿,我身边不少iOS老手都在观望。大家心里都清楚:一个全新的UI框架,想完全替代UIKit,至少得等三五年。但业务不等人啊。
这时候,Apple给了我们一座桥——UIHostingController。它本质上是一个UIViewController的子类,专门用来承载SwiftUI的View。你想想看,这意味着什么?
意味着你可以在一个跑了五六年的UIKit项目里,悄无声息地插入一个SwiftUI写的页面。不需要重写整个项目,不需要推翻现有架构。我个人习惯把这种策略叫做「渐进式迁移」。
核心结论:UIHostingController 是 UIKit 和 SwiftUI 之间的「翻译官」。它让两个框架能在同一个 ViewController 栈里共存。
2.2 第一个嵌入示例:三行代码搞定
我们先来点实际的。假设你有一个传统的 UIKit 项目,现在想在某个按钮点击后,弹出一个 SwiftUI 写的设置页面。
SwiftUI 的视图长这样:
import SwiftUI
struct SettingsView: View {
var body: some View {
VStack(spacing: 20) {
Text("设置中心")
.font(.largeTitle)
Text("这是用 SwiftUI 写的页面")
.foregroundColor(.gray)
}
.padding()
}
}
然后在 UIKit 的 ViewController 里,你只需要这样做:
import UIKit
import SwiftUI
class MainViewController: UIViewController {
@IBAction func showSettingsTapped(_ sender: UIButton) {
let settingsView = SettingsView()
let hostingController = UIHostingController(rootView: settingsView)
navigationController?.pushViewController(hostingController, animated: true)
}
}
嗯,就这么简单。三行核心代码,一个 SwiftUI 页面就嵌进去了。你甚至不需要关心 SwiftUI 的布局是怎么算的,UIHostingController 会自动帮你处理。
小提示:别忘了在 UIKit 的文件里 import SwiftUI。我刚开始就经常忘,编译器报错半天才发现。
2.3 生命周期管理:谁管谁?
这里有个坑,我踩过。很多开发者以为 SwiftUI 的 View 有自己的生命周期,和 UIKit 的 ViewController 是两套体系。其实不完全对。
UIHostingController 本身是一个 UIViewController,所以它遵循 UIKit 的生命周期:
viewDidLoad→ 加载 SwiftUI 视图viewWillAppear→ SwiftUI 的onAppear被触发viewDidDisappear→ SwiftUI 的onDisappear被触发deinit→ SwiftUI 视图被销毁
说白了,SwiftUI 的 onAppear 和 onDisappear 只是 UIKit 生命周期事件的「影子」。你可以在 SwiftUI 里这样监听:
struct SettingsView: View {
var body: some View {
Text("设置页面")
.onAppear {
print("SwiftUI 视图出现了")
// 可以在这里做数据加载
}
.onDisappear {
print("SwiftUI 视图消失了")
// 可以在这里做清理工作
}
}
}
我曾经在项目里遇到一个问题:SwiftUI 的 onAppear 被调用了两次。排查了半天,发现是因为父 ViewController 在 viewWillAppear 里又 push 了一次同一个 hostingController。所以记住:UIHostingController 的生命周期,由 UIKit 的导航栈决定。
注意:不要在 SwiftUI 的 onAppear 里做太重的操作。因为 UIKit 的 viewWillAppear 可能会被频繁调用,比如从后台切回前台时。
2.4 布局适配:尺寸到底听谁的?
这是另一个容易翻车的地方。SwiftUI 的布局哲学是「声明式 + 自动计算」,而 UIKit 是「frame + Auto Layout」。两者混在一起,尺寸怎么算?
UIHostingController 默认会尝试根据 SwiftUI 视图的内容,自动计算自己的 preferredContentSize。但如果你把它放在一个固定 frame 的容器里,比如:
let hostingController = UIHostingController(rootView: SettingsView())
hostingController.view.frame = CGRect(x: 0, y: 0, width: 300, height: 400)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
这时候,SwiftUI 视图会尝试填满你给的 frame。但如果 SwiftUI 内部用了 Spacer() 或者 .frame() 修饰符,可能会出现意想不到的留白或裁剪。
我建议的做法是:让 SwiftUI 自己决定大小,UIKit 只提供容器。
比如用 Auto Layout 约束:
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
这样 SwiftUI 视图会自适应容器的大小,同时保持内部布局的灵活性。
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 全屏 Push | 直接 push hostingController | 自动适配导航栏和屏幕 |
| 作为子视图 | 用 Auto Layout 约束 | 避免 frame 硬编码 |
| 弹窗/Popover | 设置 preferredContentSize | SwiftUI 自动计算最佳尺寸 |
| TableViewCell 内嵌 | 使用 UIHostingConfiguration(iOS 16+) | 性能更好,自动复用 |
避坑指南:我曾经在 iOS 14 上遇到过 UIHostingController 的 view 在旋转屏幕后尺寸不对。解决方案是在 viewWillTransition 里手动调用 hostingController.view.setNeedsLayout()。iOS 15 之后这个问题就少多了。
2.5 数据传递:UIKit 怎么跟 SwiftUI 说话?
嵌入只是第一步,数据怎么传?最简单的办法是用 @State 或 @Binding,但如果你在 UIKit 侧持有数据,可以用 ObservableObject。
举个例子:
class SettingsViewModel: ObservableObject {
@Published var userName: String = "未登录"
}
// UIKit 侧
let viewModel = SettingsViewModel()
viewModel.userName = "张三"
let hostingController = UIHostingController(rootView: SettingsView(viewModel: viewModel))
SwiftUI 侧:
struct SettingsView: View {
@ObservedObject var viewModel: SettingsViewModel
var body: some View {
Text("用户名:\(viewModel.userName)")
}
}
这样,UIKit 修改 viewModel.userName 时,SwiftUI 视图会自动刷新。说白了,ObservableObject 就是两个世界的「数据总线」。
2.6 小结
UIHostingController 是混合开发的第一道门。你不需要成为 SwiftUI 专家,就能在 UIKit 项目里用上它。记住三点:
- 生命周期跟着 UIKit 走,别在 SwiftUI 里做太重的操作
- 布局用 Auto Layout 约束,别硬编码 frame
- 数据传递用 ObservableObject,简单又可靠
下一章,我们会聊怎么反过来——在 SwiftUI 里嵌入 UIKit 的视图。嗯,那又是另一番风景了。