MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SwiftUI与UIKit互操作

2021-06-041.2k 阅读

SwiftUI 与 UIKit 基础概述

SwiftUI 是苹果公司在 2019 年推出的用于构建用户界面的声明式框架。它允许开发者以一种简洁、直观的方式描述用户界面的结构和外观。例如,创建一个简单的文本标签在 SwiftUI 中可以这样写:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
    }
}

这里,ContentView 结构体遵循 View 协议,body 属性返回一个 View 类型的实例,Text 是 SwiftUI 提供的一个视图组件,用于显示文本。

UIKit 则是苹果公司在 iOS 开发早期就存在的框架,它基于命令式编程范式。在 UIKit 中创建一个简单的文本标签需要更多步骤,如下所示:

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let label = UILabel(frame: CGRect(x: 100, y: 100, width: 200, height: 50))
        label.text = "Hello, UIKit!"
        view.addSubview(label)
    }
}

在这段代码中,我们创建了一个 UIViewController 的子类 ViewController,在 viewDidLoad 方法中实例化了一个 UILabel,设置其文本和位置,然后将其添加到视图控制器的视图中。

为何需要 SwiftUI 与 UIKit 互操作

随着 SwiftUI 的推出,许多开发者希望在现有的 UIKit 项目中引入 SwiftUI 的优势,比如更简洁的代码和响应式布局。同时,对于一些已经大量使用 SwiftUI 的项目,可能也需要复用 UIKit 中成熟的组件或者功能,因为并非所有的 UIKit 功能都能立即在 SwiftUI 中找到对应的实现。例如,某些第三方库可能只支持 UIKit,或者一些特定的 iOS 系统原生功能在 UIKit 中有更完善的支持。因此,实现 SwiftUI 与 UIKit 之间的互操作,可以让开发者根据项目需求灵活选择使用最合适的技术,充分利用两者的优势。

在 UIKit 中使用 SwiftUI

UIHostingController 介绍

UIHostingController 是 UIKit 与 SwiftUI 互操作的关键类之一,它允许将 SwiftUI 的 View 嵌入到 UIKit 的视图层级中。UIHostingControllerUIViewController 的子类,它的初始化方法接收一个 View 类型的参数。

简单嵌入 SwiftUI 视图到 UIKit

假设我们有一个 SwiftUI 的 ContentView 如下:

struct ContentView: View {
    var body: some View {
        Text("This is a SwiftUI view in UIKit")
           .padding()
           .background(Color.blue)
           .foregroundColor(.white)
    }
}

在 UIKit 的 ViewController 中嵌入这个 SwiftUI 视图,可以这样做:

import UIKit
import SwiftUI

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let swiftUIView = ContentView()
        let hostingController = UIHostingController(rootView: swiftUIView)
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.frame = view.bounds
        hostingController.didMove(toParent: self)
    }
}

在上述代码中,首先创建了 ContentView 的实例 swiftUIView,然后通过 UIHostingController 将其包装。接着,按照 UIKit 中添加子视图控制器的常规步骤,将 hostingController 添加为当前视图控制器的子视图控制器,并设置其视图的框架与当前视图控制器的视图框架相同。

传递数据到嵌入的 SwiftUI 视图

如果需要在 UIKit 中向嵌入的 SwiftUI 视图传递数据,可以通过在 SwiftUI 视图中定义属性来实现。例如,我们修改 ContentView 以接收一个字符串参数:

struct ContentView: View {
    var text: String
    var body: some View {
        Text(text)
           .padding()
           .background(Color.blue)
           .foregroundColor(.white)
    }
}

在 UIKit 的 ViewController 中传递数据:

import UIKit
import SwiftUI

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let data = "Data passed from UIKit"
        let swiftUIView = ContentView(text: data)
        let hostingController = UIHostingController(rootView: swiftUIView)
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.frame = view.bounds
        hostingController.didMove(toParent: self)
    }
}

这里,我们在 ViewController 中定义了一个字符串 data,并将其传递给 ContentView 的实例,从而在 SwiftUI 视图中显示从 UIKit 传递过来的数据。

处理嵌入的 SwiftUI 视图的交互

SwiftUI 视图可能包含交互,如按钮点击等。我们可以通过在 SwiftUI 视图中定义 @Binding 属性或者使用 action 闭包来处理这些交互,并在 UIKit 中作出响应。例如,我们在 ContentView 中添加一个按钮:

struct ContentView: View {
    @Binding var isButtonTapped: Bool
    var body: some View {
        VStack {
            Text(isButtonTapped? "Button was tapped" : "Button not tapped")
               .padding()
            Button("Tap me") {
                self.isButtonTapped.toggle()
            }
        }
    }
}

在 UIKit 的 ViewController 中处理这个交互:

import UIKit
import SwiftUI

class ViewController: UIViewController {
    @State var isButtonTapped = false
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let swiftUIView = ContentView(isButtonTapped: $isButtonTapped)
        let hostingController = UIHostingController(rootView: swiftUIView)
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.frame = view.bounds
        hostingController.didMove(toParent: self)
    }
}

这里,ContentView 中的 isButtonTapped 是一个 @Binding 属性,它与 ViewController 中的 @State 属性 isButtonTapped 绑定。当按钮在 SwiftUI 视图中被点击时,isButtonTapped 的值会改变,从而在 SwiftUI 视图中更新文本显示,同时也反映在 UIKit 的 ViewController 中。

在 SwiftUI 中使用 UIKit

UIViewRepresentable 协议

在 SwiftUI 中使用 UIKit 组件,需要遵循 UIViewRepresentable 协议。这个协议有两个主要方法:makeUIView(context:)updateUIView(_:context:)makeUIView(context:) 方法用于创建 UIKit 视图的实例,updateUIView(_:context:) 方法用于在 SwiftUI 视图状态变化时更新 UIKit 视图。

创建简单的 UIKit 视图桥接到 SwiftUI

假设我们要在 SwiftUI 中使用 UILabel,可以这样实现:

import SwiftUI
import UIKit

struct UILabelView: UIViewRepresentable {
    var text: String
    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.text = text
        return label
    }
    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.text = text
    }
}

然后在 SwiftUI 的视图中使用这个桥接的视图:

struct ContentView: View {
    var body: some View {
        UILabelView(text: "This is a UILabel in SwiftUI")
    }
}

UILabelView 中,makeUIView(context:) 方法创建了一个 UILabel 实例并设置其文本。updateUIView(_:context:) 方法在 text 属性变化时更新 UILabel 的文本。

处理桥接 UIKit 视图的交互

对于一些可交互的 UIKit 视图,比如 UIButton,我们需要在 UIViewRepresentable 实现中处理交互。例如:

import SwiftUI
import UIKit

struct UIButtonView: UIViewRepresentable {
    var title: String
    var action: () -> Void
    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(type:.system)
        button.setTitle(title, for:.normal)
        button.addTarget(context.coordinator, action: #selector(Coordinator.handleTap), for:.touchUpInside)
        return button
    }
    func updateUIView(_ uiView: UIButton, context: Context) {
        uiView.setTitle(title, for:.normal)
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    class Coordinator: NSObject {
        var parent: UIButtonView
        init(_ parent: UIButtonView) {
            self.parent = parent
        }
        @objc func handleTap() {
            parent.action()
        }
    }
}

在 SwiftUI 视图中使用这个 UIButtonView

struct ContentView: View {
    @State var isTapped = false
    var body: some View {
        VStack {
            Text(isTapped? "Button was tapped" : "Button not tapped")
            UIButtonView(title: "Tap me", action: {
                self.isTapped.toggle()
            })
        }
    }
}

UIButtonView 中,makeUIView(context:) 方法创建了一个 UIButton,并为其添加了一个目标动作,该动作指向 Coordinator 类的 handleTap 方法。Coordinator 类持有对 UIButtonView 的引用,当按钮被点击时,handleTap 方法会调用 UIButtonViewaction 闭包,从而在 SwiftUI 视图中处理按钮点击的逻辑。

更复杂的 UIKit 组件桥接

对于一些更复杂的 UIKit 组件,比如 UITableView,桥接过程会更复杂。首先,我们需要实现 UITableViewDataSourceUITableViewDelegate 协议。例如:

import SwiftUI
import UIKit

struct TableViewView: UIViewRepresentable {
    var data: [String]
    func makeUIView(context: Context) -> UITableView {
        let tableView = UITableView()
        tableView.dataSource = context.coordinator
        tableView.delegate = context.coordinator
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        return tableView
    }
    func updateUIView(_ uiView: UITableView, context: Context) {
        uiView.reloadData()
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
        var parent: TableViewView
        init(_ parent: TableViewView) {
            self.parent = parent
        }
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return parent.data.count
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = parent.data[indexPath.row]
            return cell
        }
    }
}

在 SwiftUI 视图中使用这个 TableViewView

struct ContentView: View {
    let data = ["Item 1", "Item 2", "Item 3"]
    var body: some View {
        TableViewView(data: data)
    }
}

TableViewView 中,makeUIView(context:) 方法创建了一个 UITableView 并设置其数据源和代理为 Coordinator 实例。Coordinator 类实现了 UITableViewDataSourceUITableViewDelegate 协议的方法,以提供表格视图的数据和处理交互。updateUIView(_:context:) 方法在数据变化时重新加载表格视图。

生命周期管理与性能优化

UIViewRepresentable 生命周期

UIViewRepresentable 的实现中,makeUIView(context:) 方法只在第一次创建视图时调用,而 updateUIView(_:context:) 方法会在 SwiftUI 视图状态发生变化,导致需要更新 UIKit 视图时调用。这意味着我们应该在 makeUIView(context:) 中进行一次性的初始化操作,如注册单元格(对于 UITableView 等),而在 updateUIView(_:context:) 中进行数据更新等操作。

性能优化

  1. 避免不必要的更新:在 updateUIView(_:context:) 方法中,应该只更新那些真正发生变化的属性。例如,对于 UILabel,如果只有文本变化才更新文本,而不是每次都重新设置所有属性。
  2. 重用 UIKit 视图:对于像 UITableView 这样的视图,正确使用单元格重用机制可以显著提高性能。在 UITableViewDataSourcecellForRowAt 方法中,通过 dequeueReusableCell(withIdentifier:for:) 方法获取可重用的单元格。
  3. 减少布局计算:尽量减少在 updateUIView(_:context:) 方法中触发的布局计算。如果可能,将布局相关的操作放在 makeUIView(context:) 方法中一次性完成,除非布局需要根据 SwiftUI 视图状态动态变化。

与 UIKit 视图控制器集成

有时候,在 SwiftUI 中不仅需要使用 UIKit 的视图,还需要使用视图控制器。这可以通过 UIViewControllerRepresentable 协议来实现。例如,假设我们有一个简单的 UIViewController 子类:

import UIKit

class CustomViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.red
    }
}

在 SwiftUI 中嵌入这个视图控制器:

import SwiftUI
import UIKit

struct CustomViewControllerView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> CustomViewController {
        return CustomViewController()
    }
    func updateUIViewController(_ uiViewController: CustomViewController, context: Context) {
        // 可以在这里更新视图控制器的状态
    }
}

然后在 SwiftUI 视图中使用:

struct ContentView: View {
    var body: some View {
        CustomViewControllerView()
    }
}

CustomViewControllerView 中,makeUIViewController(context:) 方法创建了 CustomViewController 的实例,updateUIViewController(_:context:) 方法可以用于在 SwiftUI 视图状态变化时更新视图控制器的状态。

数据共享与同步

在 SwiftUI 与 UIKit 互操作时,数据共享和同步是关键。例如,当在 UIKit 中更新了某个数据,需要确保在 SwiftUI 视图中也能反映出这些变化。一种常见的方法是使用 @Published 属性和 ObservableObject 协议。

假设我们有一个数据模型:

import Combine

class DataModel: ObservableObject {
    @Published var value: String = "Initial value"
}

在 UIKit 中更新数据:

import UIKit
import Combine

class ViewController: UIViewController {
    var dataModel: DataModel
    init(dataModel: DataModel) {
        self.dataModel = dataModel
        super.init(nibName: nil, bundle: nil)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton(type:.system)
        button.setTitle("Update data", for:.normal)
        button.addTarget(self, action: #selector(updateData), for:.touchUpInside)
        view.addSubview(button)
        button.frame = CGRect(x: 100, y: 100, width: 200, height: 50)
    }
    @objc func updateData() {
        dataModel.value = "Data updated from UIKit"
    }
}

在 SwiftUI 中使用这个数据模型:

import SwiftUI

struct ContentView: View {
    @ObservedObject var dataModel: DataModel
    var body: some View {
        VStack {
            Text(dataModel.value)
        }
    }
}

这里,DataModel 类遵循 ObservableObject 协议,其 value 属性使用 @Published 标记。当在 UIKit 中更新 dataModelvalue 属性时,SwiftUI 视图会自动更新以反映这些变化。

常见问题与解决方案

  1. 布局问题:SwiftUI 和 UIKit 使用不同的布局系统,在互操作时可能会出现布局冲突。例如,嵌入到 UIKit 中的 SwiftUI 视图可能无法正确适配大小。解决方案是仔细设置视图的框架和约束,在 UIKit 中嵌入 SwiftUI 视图时,确保 UIHostingController 的视图框架设置正确,并且在 SwiftUI 视图中使用合适的布局修饰符。
  2. 内存管理问题:如果在 UIViewRepresentableUIViewControllerRepresentable 中创建了大量的 UIKit 资源,可能会导致内存泄漏。确保在 UIViewRepresentableupdateUIView(_:context:) 方法中正确释放不再使用的资源,并且在 UIViewControllerRepresentable 中正确处理视图控制器的生命周期。
  3. 交互传递问题:在处理从 UIKit 到 SwiftUI 或反之的交互传递时,可能会出现逻辑错误。仔细检查 UIViewRepresentableCoordinator 类的实现,确保正确处理 UIKit 视图的交互并将其传递到 SwiftUI 视图中。同样,在 UIKit 中嵌入 SwiftUI 视图时,确保正确处理 SwiftUI 视图的交互。

通过深入理解 SwiftUI 与 UIKit 互操作的原理和方法,开发者可以在项目中灵活使用这两种技术,充分发挥它们的优势,创建出更加丰富和高效的用户界面。无论是在现有 UIKit 项目中逐步引入 SwiftUI 的特性,还是在 SwiftUI 项目中复用 UIKit 的成熟组件,掌握互操作技术都能为开发者带来更大的灵活性和效率提升。