前言
一般老项目都是基于 UIKit 的,随着 SwiftUI 越来越成熟,未来的趋势会趋向于使用 SwiftUI 来进行开,所以,项目的逐步迁移至 SwiftUI 也变得有必要起来。这篇文章会展示如何在 UIKit 项目中,接入 SwiftUI。而 SwiftUI 在有些地方支持的还不是特别好,所以可能会有需要在 SwiftUI 中再去引如 UIKit 去解决目前 SwiftUI 不好解决的问题,比如富文本,所以也会介绍在 SwiftUI 中如何去加载 UIKit 的视图。
UIKit 接入 SwiftUI
1.Push 到一个 SwiftUI 视图
假设有一个 UIKit 的 ViewControllerA,然后我们新建一个 SwiftUI 的 SwiftUIViewB,然后从 A push 到 B。
首先在这里新建一个 SwiftUIViewB。
然后,给 A 添加一个按钮,创建的代码就不写了,响应的方法为:
@IBAction func pushAction(_ sender: UIButton) {
let swiftUIViewController = UIHostingController(rootView: SwiftUIViewB())
navigationController?.pushViewController(swiftUIViewController, animated: true)
}
UIHostingController 是继承自 UIViewController 的,其实就是使用 UIHostingController 将 SwiftUIViewB 给包裹起来,类似于 UIKit 的 UIViewController -> UIVIew 的方式,将 SwiftUI 的视图,当成 UIViewController 的 UIView 一样来进行使用。
效果如下:
2. 在 UIKit 视图中嵌套一个 SwiftUI 视图
改一下 SwiftUIViewB 的代码:
struct SwiftUIViewB: View {
var body: some View {
ZStack {
Color.cyan
Text("Hello, World!")
.foregroundColor(Color.white)
}
}
}
然后在 A 中:
override func viewDidLoad() {
super.viewDidLoad()
let sView = SwiftUIViewB()
let hostingController = UIHostingController(rootView: sView)
self.addChild(hostingController)
self.view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
hostingController.view.snp.makeConstraints({ make in
make.centerX.equalTo(view)
make.centerY.equalTo(view).offset(100)
make.width.equalTo(200)
make.height.equalTo(100)
})
}
一样的,借助于 UIHostingController,将 SwiftUI 的视图当做一个 UIKit 的 UIView 来使用。
效果如下:
3.值传递
来尝试一下改变 SwiftUI 视图的背景色,还是 A 和 B,我们需要在 A 中去改变 B 的背景色。
首先,在 SwiftUIViewB 中新增一个 Color 属性:
struct SwiftUIViewB: View {
var bColor: Color
var body: some View {
ZStack {
bColor
Text("SwiftUI Screen")
}
}
}
然后我们构建一个属于 SwiftUIViewB 的控制器来管理它:
class SwiftUIViewBController: UIViewController {
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
hostingController = UIHostingController(rootView: SwiftUIViewB(bColor: bColor))
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad(
self.addChild(hostingController)
self.view.addSubview(hostingController.view)
hostingController.view.snp.makeConstraints { $0.edges.equalToSuperview() }
}
var bColor: Color = .white {
didSet {
update()
}
}
let hostingController: UIHostingController<SwiftUIViewB>
func update() {
hostingController.rootView = SwiftUIViewB(bColor: bColor)
}
}
然后在 A 中:
let hostingController = SwiftUIViewBController()
hostingController.bColor = .red
navigationController?.pushViewController(hostingController, animated: true)
4.监听值更改并自动刷新 UI
在第三点中,我们是在 bColor 的 didSet 方法中,手动去刷新 UI。SwiftUI 提供了当值发生改变时,UI 自动刷新的方式,通过 ObservableObject,当然不止一种,我们这里先只说这种。
加一个自定义的数据:
class SwiftUIViewBConfig: ObservableObject {
@Published var textColor: Color
init (textColor: Color) {
self.textColor = textColor
}
}
然后改变一下 SwiftUIViewB 的代码,让它来使用这个数据:
struct SwiftUIViewB: View {
var config: SwiftUIViewBConfig
var body: some View {
ZStack {
Text("SwiftUI Screen")
.foregroundColor(config.textColor)
}
}
}
然后在 A 控制器中进行使用,简单点直接使用 UIHostingController。
class ViewController: UIViewController {
let swiftUIBViewConfig = SwiftUIViewBConfig(textColor: .black)
@IBAction func pushAction(_ sender: UIButton) {
let hostingController = UIHostingController(rootView: SwiftUIViewB(config: swiftUIBViewConfig))
navigationController?.pushViewController(hostingController, animated: true)
}
@IBAction func changeColorAction(_ sender: UIButton) {
swiftUIBViewConfig.textColor = .blue
}
}
B 中的文字颜色本来是黑色,在 A 中滴点击按钮,将 config.textColor 改成蓝色,看一下效果:
SwiftUI 接入 UIKit
其实 SwiftUI 的支持还没有 UIKit 那么完善,而且最低到 13 系统,很多 SwiftUI 的特性其实支持的不好,所以遇到实在是难搞定的,建议桥接 UIKit,或者直接使用 UIKit 来实现。
要桥接也很简单,使 UIViewRepresentable 协议将 UIView 包装一下,然后就可以在 SwiftUI 中使用了,如何需要桥接 UIViewController,也有对应的 UIViewControllerRepresentable 协议。
UIViewRepresentable
这个协议有两个必须实现的方法,就是 makeUIView 和 updateUIView。
public protocol UIViewRepresentable : View where Self.Body == Never {
func makeUIView(context: Self.Context) -> Self.UIViewType
func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)
}
举个简单的例子,桥接一个 UIView 并改变它的背景颜色。
首先,先处理好需要桥接的 UIView:
struct UIKitView: UIViewRepresentable {
@Binding var bColor: UIColor
func makeUIView(context: UIViewRepresentableContext<UIKitView>) -> UIView {
let view = UIView()
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<UIKitView>) {
uiView.backgroundColor = bColor
}
}
然后在 SwiftUI 中使用它:
struct SwiftUIView: View {
@State var kitViewColor: UIColor = .blue
var body: some View {
VStack {
UIKitView(bColor: $kitViewColor)
.frame(width: 100, height: 100)
Button {
kitViewColor = .red
} label: {
Text("change color")
}
}
}
}
看一下效果:
makeUIView方法在其生命周期只会调用一次,在这个方法中返回一个你要在 SwiftUI 中表现的UIView。updateUIView方法会在UIView的状态发生变化时被调用,在生命周期内会被调用次。
在这个例子中,使用 @Binding 包装属性将 SwiftUI 中的 State 和 UIKit 的 UIKitView 绑定在一起,当 state 发生变化,就会触发 updateUIView 方法,去改变 UIKitView 的背景颜色。
目前 bColor 是从 SwiftUI 中传递到 UIKit 的,但是如果需要从 UIKit 传值到 SwiftUI 中又该如何实现呢,答案是使用 Coordinator。
SwiftUI 从 UIKit 中获取值
如果你要桥接到 SwiftUI 中的不是一个 UIView,而是一个 UITextField,而你又刚好需要实现 UITextFiled 的代理,那么你该把这个 delegate 设置成谁呢?答案是 Coordinator。
在 UIViewRepresentable 协议中,还有一个东西:
public protocol UIViewRepresentable : View where Self.Body == Never {
func makeCoordinator() -> Self.Coordinator
}
直接看如何使用:
struct CustomTextFiled: UIViewRepresentable {
@Binding var text: String
class Coordinator: NSObject, UITextFieldDelegate {
@Binding var text: String
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.backgroundColor = .cyan
textField.frame = CGRect(x: 50, y: 28, width: 200, height: 44)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
}
然后在 SwiftUI 中使用:
struct SwiftUIView: View {
@State var kitViewText: String = "123"
var body: some View {
VStack {
CustomTextFiled(text: $kitViewText)
.frame(width: 300, height: 44)
Text(kitViewText)
}
}
}
上述代码中,当 UITextFiledDelegate 的 textFielDidChangeSelection 方法触发,coordinator 或更新 text,触发 updateUIView,最终更新 UITextField。
小结
UIKit 和 SwiftUI 的相互桥接,了解之后其实并不麻烦,当然 SwiftUI 有自己的学习曲线,面向未来编程的话,建议如果仅支持 iOS13 后的 App,可以考虑使用 SwiftUI 来实现一些简单页面了。