一、为什么要集成SwiftUI和UIKit

很多做iOS开发的朋友肯定遇到过这种情况:公司的老项目用的是早期的UIKit框架,现在想做一个功能复杂的统计模块,用UIKit写要堆叠大量的布局代码和代理逻辑,而用SwiftUI的声明式语法,几行就能写出美观的界面,但又不敢全量重写项目——毕竟老项目里还有很多经过验证的成熟逻辑,全替换风险太高。这时候,SwiftUI和UIKit的集成就成了最优解:不用重构整个项目,就能快速用新框架实现需求,同时复用现有代码。

这种集成的核心价值是「渐进式开发」,既享受SwiftUI写代码快、易维护的好处,又不用放弃UIKit积累的技术资产。当然,集成也有它的局限性,后续会详细分析。

二、集成的核心工具(苹果给的「转换器」)

要实现两者的无缝对接,苹果提供了两个核心工具,就像两个转换器,能把SwiftUI的代码变成UIKit能识别的内容,反过来也一样。

2.1 从UIKit嵌入SwiftUI:用UIHostingController

如果你想在UIKit的页面里加入SwiftUI的功能,比如老项目的首页加一个新的SwiftUI banner,就用UIHostingController——这个类就像一个转换器,能把SwiftUI的View包装成一个普通的UIViewController,和UIKit的控制器完全兼容,直接加到项目里就行。

下面是完整的示例,技术栈为Swift、UIKit、SwiftUI,注释已经标好每个步骤:

import UIKit
import SwiftUI

// 这是我们要嵌入的SwiftUI自定义View,实现一个简单的计数器
struct CounterSwiftUIView: View {
    // SwiftUI的状态,用来记录点击次数
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("SwiftUI计数器:\(count)")
                .font(.title)
                .foregroundColor(.blue)
            
            Button(action: {
                // 点击按钮,状态自动更新
                count += 1
            }) {
                Text("点我加1")
                    .padding(.horizontal, 20)
                    .padding(.vertical, 10)
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

// UIKit的首页控制器,用来嵌入上面的SwiftUI View
class HomeViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        // 1. 创建SwiftUI View的实例
        let swiftUIView = CounterSwiftUIView()
        // 2. 用UIHostingController包装,变成UIKit能识别的控制器
        let hostingVC = UIHostingController(rootView: swiftUIView)
        
        // 3. 把这个控制器加到当前UIKit页面,设置Auto Layout(适配iOS不同屏幕)
        addChild(hostingVC)
        view.addSubview(hostingVC.view)
        hostingVC.view.translatesAutoresizingMaskIntoConstraints = false // 必须关,否则Auto Layout失效
        NSLayoutConstraint.activate([
            hostingVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 30),
            hostingVC.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            hostingVC.view.widthAnchor.constraint(equalToConstant: 280),
            hostingVC.view.heightAnchor.constraint(equalToConstant: 180)
        ])
        hostingVC.didMove(toParent: self) // 必须调用,让生命周期同步
    }
}

这段代码运行后,你会在UIKit的首页看到一个完整的SwiftUI计数器,点击按钮能正常增加数字,没有任何违和感。

2.2 从SwiftUI嵌入UIKit:用UIViewRepresentable

反过来,如果你想在SwiftUI的页面里复用已有的UIKit组件(比如项目里已写好的自定义地图、时间选择器),就用UIViewRepresentable——这个协议能把UIKit的View包装成SwiftUI能识别的View,就像给UIKit的View穿了件SwiftUI的衣服,能完美适配SwiftUI的声明式布局。

下面是完整示例,技术栈为Swift、UIKit、SwiftUI:

import SwiftUI
import UIKit

// 把UIKit的UILabel包装成SwiftUI能用的组件,继承UIViewRepresentable
struct CustomUILabel: UIViewRepresentable {
    // 对外暴露的属性,用来传值(类似SwiftUI的@State)
    var displayText: String
    
    // 创建UIKit View的方法,必须实现
    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 22, weight: .bold)
        label.textColor = .systemRed
        label.textAlignment = .center
        return label
    }
    
    // 更新UIKit View的方法,当SwiftUI的状态变化时调用,必须实现
    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.text = displayText
    }
}

// SwiftUI的主页面,用来展示上面的UIKit Label
struct MainSwiftUIView: View {
    // SwiftUI的状态,绑定UIKit Label的文字
    @State private var labelContent = "初始UIKit文字"
    
    var body: some View {
        VStack(spacing: 30) {
            Text("SwiftUI里嵌入UIKit组件")
                .font(.title)
            
            // 使用包装后的UIKit Label,传入要显示的文字
            CustomUILabel(displayText: labelContent)
                .frame(height: 50)
            
            Button(action: {
                // 点击按钮,更新状态,UIKit Label自动变化
                labelContent = "SwiftUI改了UIKit的文字!"
            }) {
                Text("点我更新UIKit文字")
                    .padding()
                    .background(Color.orange)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .padding()
    }
}

// 预览代码,实际项目可以删除
struct MainSwiftUIView_Previews: PreviewProvider {
    static var previews: some View {
        MainSwiftUIView()
    }
}

这个示例里,点击SwiftUI的按钮,UIKit Label的文字会同步变化,相当于把UIKit组件无缝融入了SwiftUI的布局中。

三、集成时的常见难题及解决办法

3.1 双向数据传递的坑

很多人集成时会遇到:改了SwiftUI的状态,UIKit没反应;或者改了UIKit里的数据,SwiftUI不更新。这是因为两边的状态是独立的,没有共享数据源。解决方法是:用共享的ObservableObject——可以理解成一个公共的「数据盒子」,两边都引用这个盒子,任何一方修改,另一方都会自动同步。

举个双向传值的完整例子,技术栈为Swift、Combine:

import SwiftUI
import UIKit
import Combine

// 共享的ViewModel(数据盒子),用@Published标记要监听的状态
class SharedData: ObservableObject {
    @Published var message: String = "初始消息"
}

// SwiftUI的View,监听共享数据
class MessageSwiftUIView: View {
    @ObservedObject var data: SharedData
    init(data: SharedData) { self.data = data }
    
    var body: some View {
        Text("收到的消息:\(data.message)")
            .font(.headline)
            .padding()
    }
}

// UIKit的控制器,修改共享数据
class MixedViewController: UIViewController {
    // 全局唯一的共享数据,只创建一次
    private let sharedData = SharedData()
    // 用来管理Combine的订阅
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        // 嵌入SwiftUI View,传入共享数据
        let swiftUIView = MessageSwiftUIView(data: sharedData)
        let hostingVC = UIHostingController(rootView: swiftUIView)
        
        addChild(hostingVC)
        view.addSubview(hostingVC.view)
        hostingVC.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
            hostingVC.view.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
        hostingVC.didMove(toParent: self)
        
        // 加一个UIKit按钮,点击修改共享数据
        let button = UIButton(type: .system)
        button.setTitle("UIKit改消息", for: .normal)
        button.addTarget(self, action: #selector(changeMessage), for: .touchUpInside)
        button.frame = CGRect(x: 80, y: 250, width: 200, height: 50)
        view.addSubview(button)
    }
    
    @objc private func changeMessage() {
        // 修改共享数据,SwiftUI会自动收到更新
        sharedData.message = "UIKit改了消息!"
    }
}

这个例子里,不管是UIKit还是SwiftUI修改数据,两边都会同步更新,彻底解决了状态不同步的问题。

3.2 生命周期同步的问题

UIKit和SwiftUI的生命周期逻辑不一样:比如UIKit的viewWillAppear和SwiftUI的onAppear执行时机不同,嵌入时如果没有同步,会出现数据不加载、界面错乱的情况。解决方法有两种:

  1. UIHostingController的代理方法,监听UIKit的生命周期(比如viewWillAppear时触发SwiftUI的加载方法);
  2. 在SwiftUI的View里用onAppear修饰符,对应UIKit的页面出现时机,比如在onAppear里写加载数据的代码,确保页面显示时内容到位。

3.3 布局适配的问题

集成时最常见的布局问题是:SwiftUI的View在UIKit里大小不对,或者Auto Layout失效。解决注意事项:

  1. UIHostingController时,必须把translatesAutoresizingMaskIntoConstraints设为false,否则系统会用旧的布局逻辑;
  2. UIHostingController的view设置明确的宽高约束,不要让它自适应,或者用sizeThatFits来自动适配;
  3. 颜色和字体要统一:SwiftUI的Color和UIKit的UIColor可以互相转换(比如Color(uiColor: .systemBlue)),确保两边样式一致。

四、适用场景、优缺点和注意事项

4.1 适用场景

  1. 老项目迭代:不想全重写,只需要加新功能,用SwiftUI快速实现后嵌入;
  2. 组件复用:已有成熟的UIKit组件,不用重写,包装成SwiftUI能用的组件;
  3. 跨平台准备:如果后续要支持macOS、iPadOS,集成后迁移更方便;
  4. 混合需求:团队里有人擅长UIKit,有人擅长SwiftUI,分工开发时无缝对接。

4.2 优缺点

优点:

  • 渐进式开发:重构风险低,不用一次性迁移所有代码;
  • 效率高:SwiftUI的声明式代码比UIKit少30%左右,写起来更快;
  • 状态统一:通过共享数据盒子,避免重复的状态管理逻辑。 缺点:
  • API兼容:iOS13以下不支持SwiftUI,需要做版本适配;
  • 性能损耗:复杂页面的渲染速度略慢于纯UIKit,不过普通业务场景完全够用;
  • 调试麻烦:两个框架的调试工具切换,偶尔会出现断点不命中的问题。

4.3 注意事项

  1. 明确主导框架:选UIKit或SwiftUI其中一方作为主导,不要两边混用流程控制,否则会混乱;
  2. 状态唯一:不要在两边单独存状态,一定要用共享的ViewModel;
  3. 版本适配:如果要支持iOS12及以下,要把SwiftUI的代码用@available修饰,低版本走UIKit逻辑;
  4. 内存管理:不要让UIHostingController和它的父控制器形成循环引用,用弱引用处理代理关系。

总的来说,SwiftUI和UIKit的集成是iOS开发里很实用的技巧,只要掌握核心的转换器工具,处理好数据和生命周期的同步,就能轻松实现新旧技术的无缝对接,既保留老项目的积累,又能享受新框架的便利。