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。

这个协议有两个必须实现的方法:makeUIViewupdateUIView

下面是一个将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

上面我们包装了UIActivityIndicatorUISearchBar,每次都要把makeUIViewupdateUIView这一套写一遍。

一方面如果我们项目里用的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

 

 

 

文哥博客(https://wenge365.com)属于文野个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com

备案号:粤ICP备18108585号 友情链接