SwiftUI带来了构建界面的全新范式,但是至今依然不能支持 UIView里的所有 UIControls,像是 UIActivityIndicatorView
, WKWebView
, MKMapView
以及 UIPageControl
等都没有 SwiftUI 原生的支持。
但是好在我们可以通过 UIViewRepresentable
协议将 UIView 包装后用在 SwiftUI里,同时也有 UIViewControllerRepresentable
协议来将 UIViewController 集成到 SwiftUI中。
本文目标:
- 理解
UIViewRepresentable
是如何工作的并探究它的生命周期 - 使用
Coordinator
在 SwiftUI 与 UIKit 之间传递数据;通过将UISearchBar
集成到 SwiftUI 进行说明 - 创建泛型包装器来快速集成任意 UIView 到 SwiftUI 界面中
UIViewRepresentable
协议
通过该协议就可以让我们轻松地在 SwiftUI 界面上使用 UIView
这个协议有两个必须提供的方法:makeUIView
和 updateUIView
。
下面是一个如何包装 UIActivityIndicatorView
的例子:
struct ActivityIndicator: UIViewRepresentable {
@Binding var startAnimating: Bool
func makeUIView(context: Context) -> UIActivityIndicatorView {
return UIActivityIndicatorView()
}
func updateUIView(_ uiView: UIActivityIndicatorView,
context: Context) {
if self.startAnimating {
uiView.startAnimating()
} else {
uiView.stopAnimating()
}
}
}
makeUIView
方法创建了一个要表示的 UIView, 在其生命周期里只会调用一次updateUIView
方法会在 UIView 的状态发生变化时被调用,所以在其生命周期内会被调用多次(即使这是一个空实现也会被调用多次)。这里我们通过一个@Binding
属性来控制这个活动指示器是否显示。
下面是一个应用该活动指示器的简单 SwiftUI程序,用一个按钮来控制是否显示:
通过这个 @Binding
包装属性, 我们将一个 SwiftUI state 与 ActivityIndicator
绑定在一起,当这个 state 变化,就会触发 updateUIView
方法,活动指示器就会跟着显示或者消失
使用 Coordinator
@Binding
包装属性可以让我们将数据从 SwiftUI 传递给 UIKit View,那反过来当 SwiftUI 要从 UIKit View 中获取数据,要怎么做呢?
这时候就该 Coordinator
登场了,它是一个用来实现 UIKit View 的代理的类,可以在其中实现诸如在 MKMapView
上添加 annotations,或者更新 UIPageController
的 current index 等等
下面是一个实现了 UISearchBarDelegate
的 Coordinator
类
class Coordinator: NSObject, UISearchBarDelegate {
@Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
接下来,给实现了 UIViewRepresentable
的结构体添加 makeCoordinator
方法,该方法会在 makeUIView
之前调用,会为 context 创建 coordinator,这个 context 保存的是 UIViewRepresentable
view 的当前状态,当需要创建(makeUIView
)和更新(updateUIView
)这个 view时 就会把 context 作为参数传递进去;所以我们就可以在 makeUIView
中将 context 中的 coordinator 赋给 UIView 的 delegate,代码如下:
struct SearchBarView: UIViewRepresentable {
@Binding var text: String
var placeholder: String
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: Context) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
return searchBar
}
func updateUIView(_ uiView: UISearchBar,
context: Context) {
uiView.text = text
}
}
在上面的代码中,一旦 UISearchBarDelegate
代理触发,coordinator 更新 text,也就触发了 updateUIView
,也就最终更新了 UISearchBar,下面是相应的 SwiftUI App 展示:
UIViewRepresentable
生命周期
dismantleUIView
相当于 UIView 的 deinit
方法,可以在其中做一些诸如删除通知 observer,停止 timer 等清理工作
UIViewControllerRepresentable
的生命周期也差不多,只是把相应的方法做一个替换即可
泛型 UIViewRepresentable
上面我们包装了 UIActivityIndicator
和 UISearchBar
,每次都要把 makeUIView
,updateUIView
这一套写一遍。
一方面如果我们项目里用的 UIKit View 比较多,每次都写一遍有点烦,另一方面这里有一个视图逻辑分离的问题。举例来说,UIActivityIndicator
是在 SwiftUI 里创建的,但是它动画开关逻辑却被放在了 UIViewRepresentable
里。
好在我们可以通过创建一个泛型的 UIViewRepresentable
结构体来包装任意 UIKit View,代码如下:
struct Anything<Wrapper : UIView>: UIViewRepresentable {
var makeView: () -> Wrapper
var update: (Wrapper, Context) -> Void
init(_ makeView: @escaping @autoclosure () -> Wrapper,
updater update: @escaping (Wrapper) -> Void) {
self.makeView = makeView
self.update = { view, _ in update(view) }
}
func makeUIView(context: Context) -> Wrapper {
makeView()
}
func updateUIView(_ view: Wrapper, context: Context) {
update(view, context)
}
}
@autoclosure
不是必须的,但是它可以让方法调用看上去更加舒服,因为第一个参数不需要加闭包的括号了,直接用 UIView 的表达式即可;此外,自动闭包有延迟执行的特性,只有在需要的时候才会去创建 UIView。
下面是用 Anything
来包装使用 UIActivityIndicatorView
的代码:
Anything(UIActivityIndicatorView(style: .large)) {
if shouldAnimate {
$0.startAnimating()
} else {
$0.stopAnimating()
}
}
使用 Anything
, 我们可以包装任意 UIKit View,如果需要特定的 Coordinator
,我们可以扩展上面的泛型代码来创建对应的代理方法
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。