75142913在线留言
CS193p2021学习笔记第十三课:Publisher发布者与 More Persistence更多的持久性_IOS开发_网络人

CS193p2021学习笔记第十三课:Publisher发布者与 More Persistence更多的持久性

Kwok 发表于:2021-09-08 11:08:10 点击:65 评论: 0

在上一节课的开始我们学习了Publisher的理论知道,在第十一课也学习了持久化,本课将针对这2部分内容进行加深巩固学习。

一、Publisher 发布者与订阅者

1、Publisher 发布者

Publisher只是一个协议,它实现了被包装属性产生变化后的发送(广播)的工作,如果出现问题,可能会失败(失败率极低)。发布者将值发送给一个或多个订阅者。它们符合Publisher协议,并声明输出的类型和它们产生的任何错误:

Publisher<Output,Failure> //受class的约束,所以只能在class里使用。
//当属性更改时,将在属性的willSet块中发布,这意味着订阅者在属性上实际设置新值之前收到新值。
public protocol Publisher {
    associatedtype Output
    associatedtype Failure : Error 
    func receive(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

一个发布者(Publisher)如果没有订阅者时,则不会发布任何数据。通常两种相关类型(associatedtype)来表述他:Output 和 Failure 。对应事件输出值和失败处理两种情况。发布者可以随时间发送任意数量的值,或者因错误而失败。关联类型Output定义了发布者可以发送哪些类型的值,而关联类型Failure定义了它可能失败的错误类型。发布者可以通过指定Never关联类型来声明它永远不会失败。当指定为Never时,某些发布者API会进行特别的处理。

关于发布者的详细介绍与使用,请移步:http://www.55mx.com/ios/184.html

2、Subscriber 订阅者

我们可以利用订阅者倾听它(发布者是电台,订阅者则是收音机),这里称为订阅它。

关于订阅者的详细介绍与说明,请移步到:http://www.55mx.com/ios/186.html

另一方面,订阅者订阅一个特定的发布者实例,并接收一个值流,直到订阅被取消。它们符合Subscriber协议。为了订阅发布者,订阅者的关联Input和Failure类型必须符合发布者的关联Output和Failure类型。

public protocol Subscriber : CustomCombineIdentifierConvertible {
  associatedtype Input
  associatedtype Failure : Error

  func receive(subscription: Subscription)
  func receive(_ input: Self.Input) -> Subscribers.Demand
  func receive(completion: Subscribers.Completion)
}

有的时候我们可能会发现,Publisher发布的内容与我们想要的内容相似,但不完全是我们想要的内容,因此我们也可以即时的对其类型转换。将某类型从一个发布者变成其它类型。

关于发布者与订阅是与SwiftUI一起发布的Combine 框架里面的内容,发布者和订阅者是 SwiftUI 在 UI 和底层模型之间双向同步的支柱。用于保持 UI 和Model同步,从而让我们更容易的使用 SwiftUI。

在本课程以外应该尽量的多学习关于Combine 框架的内容,这将让我们编写出非常棒和优雅的代码。掌握越多的Combine知识,你的代码将更优秀。

3、接收器.sink

本质上是Subscriber订阅者提供的功能之一,.sink是一个非常强大的简单函数,可以订阅发布信息的值,附加具有基于闭包行为的订阅者。

func sink(receiveCompletion: @escaping ((Subscribers.Completion) -> Void),
 receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
//返回一个可取消的实例,您在结束分配接收到的值时使用它。结果的解除分配将拆除订阅流。

.sink可传入2个闭包代码参数,我们需要一个接收返回AnyCancellable类型的变量,否则.sink将停止工作,像下面代码:

cancellable = myPublisher.sink(//调用发布者sink函数,可用为下面的2个参数
    receiveCompletion:{ result in ...}//完成后要执行的闭包。返回一个 Completion<Failure>枚举(case finished、case failure)
    receiveValue:{ thingThePublisherPublishes in ...}//在收到值时执行的闭包(会根据值的数量N次调用)。只有在所有值都被发布后才执行上面的闭包
)

正常情况下receiveCompletion返回finished出版商正常完成。如果出现了错误则返回一个泛型的Failure,它会告诉你为什么失败。如在调用时将失败设置为了Never,则.sink没有receiveCompletion这部分,只能使用receiveValue。

从Publisher出来,每次出现时都会执行一个闭包。在每次发布完全后执行这个闭包代码。所以在发布者上调用这个接收器函数(.sink),它会返回一个cancellable值,这是一个可取消的。

let myRange = (0...3)
cancellable = myRange.publisher
    .sink(
          receiveCompletion: { print ("发布完成: ($0)") },//最后打印:发布完成:finished
          receiveValue: { print ("值: ($0)") }//开始打印:值:0、值:1、值:2、值:3(打印4次)
    )
//此方法创建订阅者并在返回订阅者之前立即请求无限数量的值。应该保留返回值cancellable,否则流将被取消。

cancellable值实现了AnyCancellable协议,其中包括了可调用取消的功能,当不需要发布的内容时,我们可以调用cancellable.cancel()取消。cancellable值除了取消还有一个很重要的功能,可以保持.sink订阅者活跃(不会被内存回收),当返回cancellable值时.sink将在内存常跓,这是因为.sink指向了cancellable变量。

假设cancellable是一个Optional类型的AnyCancellable,当把cancellable设置为nil时它将不会指向.sink返回的任何内容。这也会导致.sink停止工作(内存回收),所以它的生命周期是绑定在一起的。就像@State与视图的关系,是相互依赖的。如果我们希望.sink继续订阅,就需要一直使用一个变量来接收.sink的返回值。

4、分配者.assign(22:10)

分配者.assign与.sink的工作是一样的,用于接受发布者的值,但和上面主动接受并执行闭包不同的是:我们可以通过.assign将来自发布者的每个元素分配给对象上的属性。

cancellable = myRange.publisher
    .assign(to:\EmojiArtDocument.backgroundImage,on:self )//将publisher发布的数据分配给 "背景图"

使用.assign的前提条件是必须将发布者的错误设置为Never。我们可以使用.replaceError(with: nil)将错误替换掉。这样不管发生任意错误值都是nil。.assign只能分配发布数据,不能执行闭包代码(这是与.sink的区别,所以我们通常情况下更多时间是在使用.sink

5、SwiftUI的订阅者 .onReceive

.onReceive可以让我们通过视图也可以侦听正在发布的内容:

.onReceive(publisher){ thingThePublisherPublishes in //每次发布的值放入thingThePublisherPublishes
    //当发布时执行此闭包代码
}

视图通过.onReceive这个Modifier实现对publisher的$版本侦听,我们只需要将需要侦听的$publisher放到参数里即可,当publisher发布时,我们通过thingThePublisherPublishes接收到被发布的值,然后执行闭包里的代码。

这也会导致UI重建,我们不经常使用.onReceive,因为我们还有一个.onChange可用(不使用$版本)。在上一课里已使用过了。通过.onChange知道我们的变量何时发生了变化。

6、Foundation发布商

上面说到的发布者是由Combine框架提供的,这里的Foundation发布商(也可以叫Foundation发布者),是由Foundation框架提供,在上一课开始我们讲到过以$开头的变量projectedValue,是一个Publisher。它是发布那个变量的发布者,每次变量产生变化时,它都会进行发布操作。所以这里从变量中获取发布者的一个非常常见的地方,即ViewModel中的@Published变量。

a.URLSession:当我们使用多线程的时候,我们使用GCD获取背景图像,原始GCD调用多线程来执行。而URLSession则是一个更高级别的API,用于有互联网上访问和获取事物。URLSession提供一个dataTaskPublisher函数,它提供一个发布者以发布最终从internet中获取到的数据。

b.Timer:它提供一个publish(every:)的函数,它每一秒或者每x秒发布一次(通过every参数设置间隔)。 它的发布机制就是发布当前的日期和时间,在其它语言里称为计时器。

c.NotificationCenter:这是一个通知中心发布器,用于通过系统给用户发送通知。这里有使用案例:http://www.55mx.com/ios/109.html

7、关于Combine(非课程内容)

Apple将Combine 简单地描述为通过组合事件处理操作符来定制异步事件处理的框架。

当您开始深入研究 Combine 的文档时,您会看到诸如Publisher、Subscriber、Operators、Cancelleable、Scheduler 之类的技术术语。很明显,Combine 框架基本上是 Apple 的函数式反应式编程(FRP)范式的实现。有了Combine,就不再需要使用任何第三方解决方案。 

使用Combine可以给我带来的好处:简单的异步代码、多线程被简化、可以轻松组合成链的业务逻辑的可组合组件。

假设我们的应用程序需要同时执行 3 个异步请求,并获取执行结果。我们可以使用 DispatchGroup 和 DispatchQueue 来解决这个复杂的任务。它需要一些样板代码来创建队列、分组、向队列添加任务以及订阅回调。如果异步任务应该返回某种结果,事情会变得更复杂一些。在这种情况下,您需要创建实例变量并从那里写入和读取值。

而我们使用Combine来处理这个问题就变成比较简单了:

Publishers.Zip3(intAsyncTask, stringAsyncTask, voidAsyncTask)
 .sink { (intValue, stringValue, _) in 
   // 任务执行完毕
}

 如果想要系统学习关于Combine框架请移步:http://www.55mx.com/ios/183.html

在下面的演示里,我们将使用上面介绍的URLSession 取代GCD调用的Data来实现同样的效果。代码更少、更易读。

二、使用URLSession替换Data

在上面的理论部分我们学习了发布商,我们将使用基于布发商的解决方案替换掉GCD调用的fetchBackgroundImageDataIfNecessary。

1、定义一个可以取消发布的接收变量以保持.sink活跃:

private var backgroundImageFetchCancellable: AnyCancellable?//当为nil时,.sink将被回收,使用AnyCancellable需要引用import Combine)

可取消变量的主要做用是与.sink连接并保持其活跃,设置为Optional类型主要目的是在新的发布之间去尝试取消前一次的未完成的发布。

我们将原来case .url(let url):后面的GCD代码替换成下面更少的代码:

backgroundImageFetchStatus = .fetching //将状态设置为抓取中
backgroundImageFetchCancellable?.cancel()//尝试取消上一次的发布(27:40)
let session = URLSession.shared //初始化一个shared类型的URL请求器
//定义一个可发布的类型保存远程获取到数据(publisher在任务完成时发布数据,任务失败并出现错误时终止。)
let publisher = session.dataTaskPublisher(for: url)//返回URL会话数据任务的Publisher(14:10)
    //dataTaskPublisher.Output输出data和response元组,通过map将数据映射到UIImage上
    .map { (data, urlResponse) in //urlResponse加载请求响应相关联的元数据。
        UIImage(data: data)//将抓取的数据转化为UIImage返回,这可能失败所以将替换错误
    }
    //当我们不使用替换错误时.sink需要传入receiveCompletion对发生错误时进行处理(25:55)
    .replaceError(with: nil)//19:00 如果发行商失败了,就设置UIImage?为nil(用提供的元素nil替换流中的任何错误。)    
    .receive(on: DispatchQueue.main) //28:13 确保所有订阅者都在主队列上工作(指定从发布服务器接收元素的调度程序。)
//将publisher的.sink与backgroundImageFetchCancellable关联
backgroundImageFetchCancellable = publisher
    //如果发布者发布则执行闭包(23:30)
    .sink { [weak self] image in //[weak self]异步执行完成后回收内存(24:08)
    //这个闭包将被搁置等待着publisher发布数据,同时它会将self保存在内存里,所以swift明确要求使用“self.属性”对其修饰
    //我们不想让此闭包一直留在于内存里,所以使用[weak self]标记为弱引用,而下面的self变成了Optional类型
    //图片有可能会抓取失败,那么数据就不会被发布,当失败时此闭包也没有存在于内存的意义。
    //图片抓取需要一定的时候,也不希望在抓取时就占用了内存
        self?.backgroundImage = image //设置背景图片
        self?.backgroundImageFetchStatus = (image != nil) ? .idle : .failed(url)//修改图片抓取状态
    }

URLSession.shared会在后台完成下载工作,我们需要使用.receive指定从publisher接收数据后的队列,当发布时,我们通过.sink做为接收器,收到数据后修改背景和成功状态。而.sink收的数据会留在内存里,我们需要通过[weak self]修饰image用完就删除数据。

session.dataTaskPublisher 做为一个抓取URL数据的发布者,在今后的开发中常常使用这种方式获取服务器上的数据,我们要深入学习了解使用方式。

2、使用.onReceive让图片自动调整大小(33:02)

在上面理论里我们说到过视图里也可以侦听到发布者发布的内容,我们通过在视图里侦听到发布内容后可以做一些事情,例如自动缩放背景图片:

//监听发布者对$backgroundImage的数据发布并保存到image
.onReceive(document.$backgroundImage){ image in //$document意味着是绑定,$backgroundImage才是发布者(34:30)
    zoomToFit(image, in: geometry.size)//将发布的数据缩放
}

三、其它持久性

1、CloudKit(38:10)

它将数据存储在苹果的云服务器里的数据库,同帐号下的设备都可以使用同步这些数据。课程只讲解其作用,不涉及学习。

CloudKit具有非常基本的数据库操作,它是通过网络连接的,如果断开网线或者网线缓慢将会影响使用。这是一种异步编程方式,通常需要用户手动同步数据。

课程里演示的创建数据库代码:

let db = CKContainer.default.public/shared/privateCloudDatabase//创建数据库并定义一个数据库类型(41:33)
let tweet = CKRecord("Tweet") //创建Tweet表对象
tweet["text"] = "字段内容"//var text = "字段内容"
//工作原理有点类似“字典类型”,它将在tweet表里自动创建一个text字段并存储“字段内容”
let tweeter = CKRecord("TweeterUser")//创建TweeterUser表对象
tweet["tweeter"] = CKReference(record:tweeter,action:.deleteSelf)//在Tweet表里引用TweeterUser
//.deleteSelf 将删除self,假设内容被删除时,引用也将会被删除。
 db.save(tweet){ (svaedRecord: CKRecord?,error: NSError?) -> Void in
     if error == nil{         
         //无错误情况
     }else if error?.errorCode == CKErrorCode.NotAuthenticated.rawValue{
         //iCloud 未认证(用户未登陆)
     }else{
         //其它错误,有29个不同的CKErrorCodes
     } 
 }

查询数据库代码:

let predicate = NSPredicate(format:"text contains %@",searchString)//创谓词器并查询searchString内容相关的
let query = CKQery(recordType:"Tweet",predicate:predicate) //打开Tweet,然后传入谓词器并将结果返回给查询器query
db.perform(query){ (records:[CKRecord]?,error:NSError?) in
    if error == nil{
        //查询结果将以数组的方式返回给CKRecord
    }else if error?.errorCode == CKErrorCode.NotAuthenticated.rawValue{
        //用户未登陆iCloud
    }else{
        //其它错误,同上
    }
}

课程里只是简单的讲解了CloudKit的使用过程,今后如果在开发中有需要可以查询专门的课程与资料。

2、CoreData(47:17)

它是一个面向对象的层,基本上位于SQL数据库之上的。SQL数据库中的所有行和所有表都创建了小的ViewModel。关于CoreData需要观看2020年视频,我将在本年度课程笔记完成后,针对未学习的CoreData部分把去年的视频记录下来。

当我们使用CoreData时就要转而做主要面向对象编程的工作,CoreData基本上使用对象来存储和检索数据库中的数据。本质上CoreData是一个与MySQL一样的关系型数据库,SQL本身不是面像对象的,而CoreData的工作是将所有SQL内容转换为API,看起来就像是在访问对象一样。

CoreData可以很好的与SwiftUI结合使用,因为我们的ViewModel是对象,CoreData提供的就是ViewModels。CoreData在数据库中提供的就是行与表,以及我们将要从代码中访问它这些对象之间创建的一个映射(map)。

我们要创建CoreData可以使用xCode里的图形编辑器来完成这个映射工作,它可以映射到行和表的所有字段和变量,在图形界面下拖拽以创建这些表和行之间的关系。

我们在Xcode通过拖拽会生成一堆的class,而这些class就会成为代码中的ViewModel。通过CoreData我们可以实现CRUD(增、删、改、查)工作。

CoreData提供一个强大的属性包装器@FetchRequest为我们获取对象,它像上面CloudKit中看到的常设查询的方式工作。

四、课后总结

本课的大部分时间都是以理论为主,通过对Publisher的介绍,并告诉我们可以深入学习Combine框架对于我们今后开发中提升技术是很有帮助的,通过使用URLSession替换了原先的Data,并延伸学习了相关的.sink、.assign和.onReceive的使用。

除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/159
标签:cs193pCombinePublisherKwok最后编辑于:2021-12-20 20:20:26
0
感谢打赏!

《CS193p2021学习笔记第十三课:Publisher发布者与 More Persistence更多的持久性》的网友评论(0)

本站推荐阅读

热门点击文章