SwiftUI开发总结combine原理简单示例详解

引言

最近在自研一个新的项目,在考虑使用的技术栈时,调研了许多,比如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<UserData, Never>()
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。如图所示:

总结

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

以上就是SwiftUI开发总结combine原理简单示例详解的详细内容,更多关于SwiftUI开发combine原理的资料请关注编程教程其它相关文章!

下一章:swift依赖注入和依赖注入容器详解

 什么是控制反转(Inversion of Control)?控制反转就是把传统的控制逻辑委托给另一个类或框架来处理,客户方只需实现具体的任务而不需要关心控制逻辑。举个例子,比如存在客户方和服 ...