在成熟项目中平滑迁移,实现 UIKit 和 SwiftUI 的无缝协作。渐进式迁移是大型项目的最佳选择。
1. 混编策略
迁移路径选择
| 策略 | 适用场景 | 风险 |
|---|---|---|
| 全新模块用 SwiftUI | 新功能开发 | 低 |
| 逐页面迁移 | 中型项目 | 中 |
| 组件级混用 | 大型项目 | 低 |
| 完全重写 | 小型项目 | 高 |
推荐方案
现有 UIKit 项目
│
├── 新功能 → SwiftUI
│
├── 简单页面 → 逐步迁移到 SwiftUI
│
└── 复杂页面 → 保留 UIKit,嵌入 SwiftUI 组件
2. UIViewRepresentable
在 SwiftUI 中使用 UIKit 视图。
基础结构
struct MapView: UIViewRepresentable {
@Binding var region: MKCoordinateRegion
// 1. 创建 UIView
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
// 2. 更新 UIView(SwiftUI 状态变化时调用)
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.setRegion(region, animated: true)
}
// 3. 创建 Coordinator(处理代理)
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// 4. Coordinator 类
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
parent.region = mapView.region
}
}
}
// 使用
struct ContentView: View {
@State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 31.23, longitude: 121.47),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
)
var body: some View {
MapView(region: $region)
.ignoresSafeArea()
}
}
WKWebView 封装
struct WebView: UIViewRepresentable {
let url: URL
@Binding var isLoading: Bool
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.isLoading = false
}
}
}
处理 UITextField
struct CustomTextField: UIViewRepresentable {
@Binding var text: String
var placeholder: String
var onCommit: () -> Void
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.placeholder = placeholder
textField.borderStyle = .roundedRect
textField.delegate = context.coordinator
textField.addTarget(
context.coordinator,
action: #selector(Coordinator.textChanged),
for: .editingChanged
)
return textField
}
func updateUIView(_ textField: UITextField, context: Context) {
textField.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomTextField
init(_ parent: CustomTextField) {
self.parent = parent
}
@objc func textChanged(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
parent.onCommit()
textField.resignFirstResponder()
return true
}
}
}
3. UIViewControllerRepresentable
在 SwiftUI 中使用 UIViewController。
系统选择器
struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
@Environment(\.dismiss) var dismiss
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
parent.selectedImage = image
}
parent.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.dismiss()
}
}
}
// 使用
struct ContentView: View {
@State private var showPicker = false
@State private var image: UIImage?
var body: some View {
VStack {
if let image {
Image(uiImage: image)
.resizable()
.scaledToFit()
}
Button("选择图片") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ImagePicker(selectedImage: $image)
}
}
}
文档选择器
struct DocumentPicker: UIViewControllerRepresentable {
@Binding var fileURL: URL?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf, .text])
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
let parent: DocumentPicker
init(_ parent: DocumentPicker) {
self.parent = parent
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
parent.fileURL = urls.first
}
}
}
4. UIHostingController
在 UIKit 中使用 SwiftUI 视图。
基础用法
// SwiftUI 视图
struct ProfileView: View {
let user: User
var body: some View {
VStack {
Image(systemName: "person.circle.fill")
.font(.system(size: 80))
Text(user.name)
.font(.title)
}
}
}
// 在 UIKit 中使用
class ProfileViewController: UIViewController {
var user: User!
override func viewDidLoad() {
super.viewDidLoad()
let swiftUIView = ProfileView(user: user)
let hostingController = UIHostingController(rootView: swiftUIView)
// 添加为子控制器
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
// 设置约束
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
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)
])
}
}
在 UITableViewCell 中使用
class SwiftUITableViewCell: UITableViewCell {
private var hostingController: UIHostingController<AnyView>?
func configure<Content: View>(with view: Content, parent: UIViewController) {
// 移除旧的
hostingController?.view.removeFromSuperview()
hostingController?.removeFromParent()
// 添加新的
let hosting = UIHostingController(rootView: AnyView(view))
hostingController = hosting
parent.addChild(hosting)
contentView.addSubview(hosting.view)
hosting.didMove(toParent: parent)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hosting.view.topAnchor.constraint(equalTo: contentView.topAnchor),
hosting.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
hosting.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
导航集成
class MainViewController: UINavigationController {
func pushSwiftUIView() {
let swiftUIView = DetailView(item: selectedItem)
let hostingController = UIHostingController(rootView: swiftUIView)
hostingController.title = "详情"
pushViewController(hostingController, animated: true)
}
}
5. 数据传递
UIKit → SwiftUI
// 使用 @ObservableObject 共享数据
class SharedData: ObservableObject {
@Published var message = ""
}
class UIKitViewController: UIViewController {
let sharedData = SharedData()
func showSwiftUIView() {
let view = SwiftUIView()
.environmentObject(sharedData)
let hosting = UIHostingController(rootView: view)
present(hosting, animated: true)
}
func updateData() {
sharedData.message = "来自 UIKit 的消息"
}
}
struct SwiftUIView: View {
@EnvironmentObject var data: SharedData
var body: some View {
Text(data.message)
}
}
SwiftUI → UIKit(通过 Coordinator)
struct CustomView: UIViewRepresentable {
var onTap: () -> Void
func makeUIView(context: Context) -> UIButton {
let button = UIButton(type: .system)
button.setTitle("点击", for: .normal)
button.addTarget(context.coordinator, action: #selector(Coordinator.handleTap), for: .touchUpInside)
return button
}
func updateUIView(_ uiView: UIButton, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(onTap: onTap)
}
class Coordinator: NSObject {
var onTap: () -> Void
init(onTap: @escaping () -> Void) {
self.onTap = onTap
}
@objc func handleTap() {
onTap()
}
}
}
6. 最佳实践
✅ 推荐
// 1. 使用 Coordinator 处理代理
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// 2. 正确管理 HostingController 生命周期
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
// 3. 使用 @ObservableObject 共享状态
class SharedState: ObservableObject {
@Published var data: [Item] = []
}
// 4. 保持视图层薄,逻辑放 ViewModel
❌ 避免
// 1. 不要频繁创建 HostingController
// ❌ 在 cellForRowAt 中每次创建
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let hosting = UIHostingController(rootView: ItemView()) // ❌ 性能问题
}
// 2. 不要忘记移除子控制器
hostingController.willMove(toParent: nil)
hostingController.view.removeFromSuperview()
hostingController.removeFromParent()
// 3. 不要混用多种状态管理方案
// 选择一种主要方案:@ObservableObject 或 Combine