SwiftUI开发总结(一) 这大概是最容易理解的combine

语言: CN / TW / HK

最近在自研一个新的项目,在考虑使用的技术栈时,调研了许多,比如react-native,flutter,以及端原生的oc跟swift,但是最终选择了swiftUI + combine,之所以有如此决定,一方面是希望可以完善自己对于iOS系统开发的技术完整性,另一方面希望了解iOS开发未来的一个技术方向,那么闲言少叙切入正题。什么是swiftUI?

SwiftUI是什么?

更准确地解释可以移步到苹果开发者中心,概念性的东西,这里不做过多介绍。通过对其的一段时间开发,个人总结,swiftUI绝不是swift+UI这么简单的概念,从设计上,swiftUI十分趋近于web前端,苹果似乎有意将swift做得更加简化,swiftUI也是将开发者得注意力从之前无穷尽地修改UI转到更加关注其app内部的逻辑处理。

简而言之,如果你的项目需求崇尚极简主义,注重逻辑而不采用复杂且臃肿的交互设计,那么swiftUI绝对是值得一试的技术手段。

对于swiftUI的各个组件,官方都给出的事例,这里先不做研究,之后我会在自研项目上线之后,对于其中所用到的组件,遇到的问题,进行逐步汇总,其中会有一些在国内论坛并不容易找到的问题答案。但是现在我们先从基础数据入手,我们先了解一下什么是combine

如何理解combine

谈到combine不得不提的就是swift中的属性修饰器-- @propertyWrapper

@propertyWrapper

实话实说,如果你还没有用过propertyWrapper,那一定要尝试的使用一下,因为这个功能确实太好用了,这里引用官方解释的一段话:

For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property. When you use a property wrapper, you write the management code once when you define the wrapper, and then reuse that management code by applying it to multiple properties.

塑料翻译:

例如,如果你要为数据存储的一些基础属性提供线程安全或者存储它们,你不得不在每一个属性中都写同样的方法,这会让代码变得十分恶心。但是当你使用propertyWrapper时,当你为操作代码定义了一个修饰器,那么这些操作代码会应用在它修饰的多个属性中。

上面的解释,是我在学习propertyWrapper所能看到的最为通俗的解释。下面也是提供了一段官方代码,帮助理解。

@propertyWrapper struct TwelveOrLess {    private var number = 0    var wrappedValue: Int {        get { return number }        set { number = min(newValue, 12) }    } } struct SmallRectangle {    @TwelveOrLess var height: Int    @TwelveOrLess var width: Int } ​ var rectangle = SmallRectangle() print(rectangle.height) // Prints "0" ​ rectangle.height = 10 print(rectangle.height) // Prints "10" ​ rectangle.height = 24 print(rectangle.height) // Prints "12"

简单解释一下上面的代码,声明一个属性修饰器TwelveOrLess,内部的逻辑是输出的属性都比12小,如果大于12则输出12。

下面的SmallRectangle包装了两个属性heightwidth,当我们为这两个属性赋值,再调用get方法时,可以看到,我们的逻辑代码生效了,输出数字被控制在小于或等于12的值。

无需多余代码,属性修饰器给了swift开发者更多的想象空间。

简单的介绍了一下propertyWrapper,接下来我们回归正题,继续说回combine

Publishers 与 subscribers

如果想使用combine就不得不了解两个概念,Publishers 与 subscribers。如果你之前有做过Rxswift,或者对于RAC有一定了解的话,对于这两个概念一定不陌生。即便是对于上述框架并不了解,想要理解Publisherssubscribers也不难,因为可以把它理解为观察者模式中的发送者与监听者。

由于官方的事例采用的是通知中心的demo,这在我初学combine时给我带来了极大的困扰,因此,本文的事例并不打算采用官方事例,避免给读者带来同样的困扰。而是通过一段自己的部分开源代码对其进行讲解。

struct XXAssetModel{    var id = UUID()    var currency: Int } class XXResourceViewModel: ObservableObject {      @Published var myAsset: XXAssetModel = UserData.userCurrency fileprivate func editCurrency() {        myAsset.currency = myAsset.currency + 10      } } struct ConverterView : View {    @ObservedObject var viewModel = XXResourceViewModel() var body: some View {         return Text(viewModel.myAsset.currency) } }

这个例子相对简单,便于入门,我们来看一下,首先,在XXResourceViewModel中声明一个被 @Published修饰的属性myAsset,因为我们刚刚已经介绍过属性修饰器了,所以应该不难理解这个修饰的作用。下面引用官方的一段话。

Add the @Published annotation to a property of one of your own types. In doing so, the property gains a publisher that emits an event whenever the property’s value changes.

将 @Published 注释添加到类中的属性。这样做使该属性成为了一个publisher,只要该属性的值发生变化,publisher就会发出一个事件。

回到上面一段代码,publisher就像是电影《风声》中的老鬼,他的责任就是将自己获取的情报传递给他的上级老枪,那么,谁是subscribers老枪。上例中,Text控件就是老枪。他与viewModel.myAsset.currency形成了一种绑定关系,一旦viewModel.myAsset.currency发生改变,Text接收到信号之后,就会做出对应行动。

看到这有没有人在想到了一种设计模式?没错,就是MVVM。

Subject的使用

combine作为苹果官方推出的响应式编程框架,很大程度的融合了其他响应式编程框架的优点。除了这种自动发送信号的publisher,还有一种可以主动发送信号的Subject,看一下下面的例子。

``` final class UserData: ObservableObject {    let objectWillChange = PassthroughSubject()      var allCurrencies: [Currency] {        didSet {            objectWillChange.send(self)        }    } }

struct ConverterView : View {    @EnvironmentObject var userData: UserData var body: some View {         return list(userData.allCurrencies) {         Item()         } } } ```

UserData作为信号发送方,没有采用publisher的方式,而是利用重写set方法对其进行了主动发送。

当然如何选择要具体问题,具体分析,苹果提供了相对丰富的方法,应对不同的使用场景。

Operators的使用

当然不只是监听信号这么简单,苹果还为开发者提供了多种Operators,意在更加轻松的让开发者完成函数式编程。代码如下:

static func request(_ kind: XXKind, _ queryItems: [URLQueryItem]?) -> AnyPublisher<XXResource, Error> {        guard var components = URLComponents(url: baseUrl.appendingPathComponent(kind.rawValue), resolvingAgainstBaseURL: true)            else { fatalError("Couldn't create URLComponents") }        components.queryItems = queryItems ​        let request = URLRequest(url: components.url!) ​        return apiClient.run(request)            .map(.value) // 为XXResource中定义的实际值            .eraseToAnyPublisher()    }

上述例子中,将返回的数据,通过map()函数进行了过滤操作,提取出返回值中value的数据,并将其发送给subscribers。如图所示:

op_map.svg

总结

本文作为SwiftUI学习的第一章,着重的介绍了combine及其使用方法。文章主要以实战为主,少了许多花里胡哨的介绍跟修饰,希望可以让同学们可以更加快速容易的理解。如开头所说,后续还会总结一下swiftUI中控件在使用时,与正常UIKit不太一样的坑。毕竟国内对于swiftUI的学习并不多,所以希望可以跟同学们一同进步。

respect!!!