SwiftUI中使用"UIViewRepresentable"桥接UIKit View
所属分类:ios | 发布于 2023-06-06 09:15:24
这也是一篇摘录的文章,原封不动的手敲一遍,技术文章怎么可以写的这么好,循序渐进,简洁易懂。原文地址放在最后,原文如下:
SwiftUI带来了构建界面的全新范式,但是至今依然不能支持UIView里的所有UIControls,像是UIActivityIndicatorView, WKWebView, MKMapView以及UIPageControl等,都没有SwiftUI原生的支持。
但是好在我们可以通过UIViewPresentable协议将UIView包装后用在SwiftUI里,同时也有UIViewControllerRepresentable协议来讲UIViewController集成到SwiftUI中。
本文目标:
- 理解UIViewRepresentable是如何工作的并探究它的生命周期
- 使用Coordinator在SwiftUI与UIKit之间传递数据;通过UISearchBar集成到SwiftUI进行说明
- 创建泛型包装器来快速集成任意UIView到SwiftUI界面中
UIViewRepresentable协议
通过该协议就可以让我们轻松地在SwiftUI界面上使用UIView。
这个协议有两个必须实现的方法:makeUIView和updateUIView。
下面是一个将UIView的UIActivityIndicatorView包装成可在SwiftUI中使用的例子:
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等等。
下面是一个实现了UISerarchBarDelegate的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,我们可以扩展上面的泛型代码来创建对应的代理方法。
原文链接:https://segmentfault.com/a/1190000037510116