使用Apple Combine进行远程异步获取的API



Combine是一个功能性的响应式Swift框架,最近已在所有Apple平台(包括Xcode 11 。 使用合并,很容易处理随时间推移异步出现的值序列。 通过放弃委托和复杂的嵌套回调 ,它还简化了异步代码。

但是,一开始学习Combine似乎并不那么简单。 事实是, 合并的主要“参与者”是诸如“ 发布者”,“ 发布者”,“订阅者”, 订阅者和操作 “操作 ”之类的抽象概念,没有它们,就不可能进一步理解合并功能的逻辑。 但是,由于Apple为开发人员提供了现成的“发布者”,“订阅者”和操作员,因此使用Combine编写的代码非常紧凑且易于阅读。

您将在一个与现在从非常流行的TMDb数据库中异步检索电影信息有关的应用程序示例中看到这一点。 我们将创建两个不同的应用程序: UIKitSwiftUI ,并展示Combine如何与它们一起工作。



希望本文使您能够更轻松地学习Combine 。 在Github上可以找到本文开发的所有应用程序的代码。

合并有几个主要组成部分:

发布者Publisher




出版商出版商所有人提供价值的 TYPES。 Publisher的 “ publisher”概念是在Combine中作为协议而非特定的TYPE实现的。 Publisher协议已为输出值和失败错误关联了通用类型。
永不发布错误的“发布者”将“永不类型用于失败错误。





Apple为开发人员提供了现成的“发布者”的特定实现: JustFutureEmptyDeferredSequence@Published等。 “发布者”也被添加到Foundation类中: URLSession ,< NotificationCenterTimer

“订户” 订户




它也是一个协议协议 ,为“预订”来自“发布者”的提供了接口。 它具有与输入故障错误相关的通用类型。 显然,发布者发布者订阅者订阅者的类型必须匹配。



对于任何发布者发布者,都有两个内置的订阅者 :接收分配



“ Subscriber”接收基于两个闭包:一个闭包ReceiveValue在获取时执行,第二闭包receiveCompletion在“发布”完成时(通常或有错误)执行。



“订户” 分配将接收到的每个值前进到指定的key Path

“订阅” 订阅




首先,“发布者” 发布 通过其“ 接收”(订阅:)方法创建“订阅” 订阅并将其交付给“订阅者” 订阅者



之后, 订阅可以使用以下两种方法将其发送订阅者的“ 订阅者”:



如果您已完成Subscription Subscription的使用 ,则可以调用其cancel()方法:



“主题” 主题




这是一个协议协议 ,为“发布者”和“订户”这两个客户端都提供了接口。 本质上,“主题” 主题是“发布者” Publisher ,它可以接受输入值Input ,您可以通过调用send()方法将其用于将 “注入”到流中。 在组合模型中调整现有命令式代码时,此功能很有用。

操作




使用运算符,您可以通过转换,过滤甚至合并许多以前的upstream 发布者 ,从另一个发布者 “发布者”创建一个新的发布者“发布者”。



您会在此处看到许多熟悉的运算符名称: compactMapmapfilterdropFirstappend

基金会发行人基金会中内置了发行人。


Apple还为开发人员提供了Foundation <framework(即发布者的发布者)中一些已经内置的Combine功能,用于执行以下任务:使用URLSession提取数据,使用NotificationTimer处理通知以及基于KVO监视属性。 这种内置的兼容性将真正帮助我们将Combine框架集成到当前项目中。
要了解更多信息,请参阅文章“ Swift中的Ultimate Combine框架教程”

我们将如何学习与Combine结合


在本文中,我们将学习如何使用Combine框架从TMDb网站获取电影数据。 这是我们将一起学习的内容:

  • 使用“发布者” Future创建一个带有Promise的闭包,用于一个值: 或错误。
  • 使用“发布者” URLSession.datataskPublisher来“订阅”给定UR L发布的数据数据
  • 使用tryMap运算符通过另一个Publisher发布者转换数据数据
  • 使用解码运算符将数据数据转换为可解码对象,并将其发布以传输到链中的后续元素。
  • 使用接收运算符使用闭包“订阅”到发布者“发布者”。
  • 使用assign语句“订阅”到发布者“ publisher”,并将其提供的值分配给给定的key Path

初始项目


在开始之前,我们必须先注册才能在TMDb网站上接收API密钥。 您还需要从GitHub存储库下载初始项目。
确保将API密钥放在让apiKey常量的MovieStore类中。



以下是我们用来创建项目的主要构建块:

  • 1. Movie.swift文件中包含我们将在项目中使用的模型。 struct MoviesResponse的根结构实现了Decodable协议,我们在将JSON数据解码为Model时将使用该协议。 MoviesResponse结构具有results属性,该属性还实现了Decodable协议,并且是电影[Movie]的集合。 是她使我们感兴趣:



  • 2.枚举MovieStoreAPIError实施Error协议。 我们的API将使用此枚举来表示各种错误: URL 检索错误urlErrorencodingError 解码错误和responseError数据获取错误。



  • 3.我们的API具有一个MovieService协议,该协议具有单个方法fetchMovies(来自端点:Endpoint) ,该方法根据端点参数选择[Movie]电影。 端点本身是一个枚举枚举,代表用于访问TMDb数据库以获取诸如nowPlaying (最新), 流行 (受欢迎), topRated (顶部)和即将上映 (即将在屏幕上)之类的电影的端点



  • 4. MovieStore 是一个特定的类,该类实现MovieService协议以从TMDb站点获取数据。 在此类内部,我们使用Combine来实现fetchMovies(...)方法。



  • 5. MovieListViewController类是主要的ViewController类,在该类中,我们使用接收方法“订阅” fetchMovies(...)电影fetch方法,该方法将返回Future “发布者”,然后使用新的电影数据更新TableView表使用新的DiffableDataSourceSnapshot API 电影

在开始之前,让我们看一下将用于远程数据检索API一些基本的Combine组件。

使用接收及其闭包“订阅”到“发布者”。


“订阅”发布者“发布者”的最简单方法是使用接收及其闭包,其中之一将在我们获得新时执行,另一种将在“发布者”完成传递值时执行



请记住,在Combine中,每个“订阅”都返回一个Cancellable ,一旦我们离开上下文,它将被删除。 为了长时间维护“订阅”,例如,以异步方式获取值,我们需要将“订阅”保存在subscription1属性中。 这使我们能够始终如一地获取所有值(7,8,3,4)

Future异步“发布”单个值: 失败错误。


Combine框架中,“发布者” Future可用于使用闭包异步获取单个TYPE的Result 。 闭包有一个参数-Promise ,它是TYPE (Result <Output,Failure>)-> Void的函数。

让我们看一个简单的示例,以了解Future发布者的功能:



我们使用TYPE Int的成功结果和TYPE Never的错误创建Future 。 在Future闭包内部,我们使用DispatchQueue.main.asyncAfter(...)将代码的执行延迟2秒,从而模拟异步行为。 在闭包内部,我们以成功的Promise (.success(...))返回Promise ,它是一个介于0100之间的随机整数Int值。 接下来,我们使用两个将来的订阅( cancellablecancellable1) ,尽管内部生成一个随机数,但两者都给出相同的结果。
1.应注意,与其他“发布者”相比,“ 合并中未来 ”的“发布者”具有某些行为特征:

  • “发布者” Future总是“发布”一个值( 或错误),从而完成了其工作。

  • 与其他主要是structvalue type )结构的“发布者”不同 Future “发布者”是一个reference type类,它作为Promise闭包的参数传递,该闭包是在Future “发布者”实例初始化后立即创建的。 也就是说,在任何订户“ 订户 ”完全订阅 Future “发布者”实例之前发送Promise闭包。 像所有其他普通发行商所要求的那样, Future的“发行人”根本不需要“订户”来运行。 这就是为什么在上面的代码中只打印一次“未来的你好!”文本的原因。

  • 与大多数其他懒惰的 “发布者”(仅在有“订阅”的情况下才“发布”)不同, 未来的 “发布者”是一个eager (急躁的)“发布者”。 只有Future发布者关闭其Promise后 ,结果才会被记住,然后传递给当前和将来的“订户”。 从上面的代码中,我们可以看到,当您反复“订阅”到将来的发布者时,尽管在闭包中使用了相同的“随机”值(在本例中为6 ,但是可以不同,但​​始终相同)。随机的int值。

Future的 “发布者”的这种逻辑使其可以成功地用于存储异步资源消耗的计算结果,并且不会打扰随后的多个“订阅”的“服务器”。

如果Future的“发布者”的这种逻辑不适合您,并且您希望您的Future被称为惰性的,并且每次您获得新的随机Int值,那么您都应该以“ 延迟 ”方式包装“ Future ”:



正如Combine所建议的那样,我们将以一种经典的方式使用Future ,即作为“共享”计算的异步“发布者”。

注意2:我们需要使用接收到异步“发布者”来对“订阅”进行更多说明。 该接收方法返回AnyCancellable ,我们一直不记得它。 这意味着Swift在离开给定上下文时销毁AnyCancellable ,这是在main thread上发生的事情。 因此,事实证明,在Promise闭包可以在main thread上开始之前, AnyCancellable被销毁了。 销毁AnyCancellable时 ,将调用其cancel方法,在这种情况下,该方法将取消“预订”。 这就是为什么我们在变量cancellable <和cancellable1Set <AnyCancellable>()中记住我们对未来的 沉没 “订阅”。


使用“ 合并”TMDb网站获取电影。


首先,打开启动项目,并转到带有空实现的MovieStore.swift文件和fetchMovies方法:



使用fetchMovies方法可以通过为TYPE Endpoint端点输入参数设置特定的值来选择各种影片。 TYPE Endpoint是一个枚举枚举,其值包含nowPlaying (当前), 即将到来 (即将进入屏幕), popular (受欢迎), topRated (顶部):



让我们从使用callback闭包初始化Future开始。 收到以后,我们将退款。



callback闭包内部,我们使用generateURL(带有endpoint:Endpoint)函数为端点输入参数的相应值生成URL



如果无法生成正确的URL ,则我们使用promise(.failure(.urlError(...))返回错误,否则,继续执行“发布者” URLSession.dataTaskPublisher

要“订阅”来自某个URL数据URL我们可以使用URLSession类中内置的datataskPublisher方法,该方法将URL作为参数,并使用元组的输出数据(数据:数据,响应:URLResponse)URLError错误返回发布者“发布者”。



要将一个Publisher发布者转换为另一Publisher发布者,请使用tryMap运算符。 与map相比, tryMap运算符可以在闭包内引发throws Error错误,这将向我们返回新的Publisher发布者。

在下一步中,我们将使用tryMap运算符检查响应响应statusCode http代码,以确保其值在200300之间。 如果不是,则抛出throws responseError 枚举MovieStoreAPIError枚举错误值。 否则(没有错误时),我们仅将接收到的数据数据返回到“发布者”链中下一个发布者



在下一步中,我们将使用解码运算符,该运算符使用JSONDecoder将先前“发布者” tryMap的输出JSON数据解码为MovieResponse <Model。



... jsonDecoder配置为特定的日期格式:



为了在main thread上执行处理,我们将使用receive(on :)运算符并将RunLoop.main作为其输入参数传递。 这将允许“订户”在main线程上获取值。



最后,我们到了转型链的尽头,在那里我们使用接收来获得对发布者“发布者”形成的“链”的“订阅” 订阅 。 要初始化Sink类的实例,我们需要两件事,尽管其中之一是可选的:

  • 关闭receiveValue:。 每当“订阅” 订阅从“发布者” 发布者接收到新时,它将被调用。

  • receiveCompletion关闭:(可选)。 在发布者 “发布者”完成发布后将调用该方法,并为其提供完成枚举,我们可以使用该枚举枚举检查值的“发布”是否确实完成或完成是由于错误引起的。

receiveValue闭包内部,我们只需调用一个带有.success选项和$ 0.results值的promise ,在我们的示例中,该值是一系列movie 电影 。 在receiveCompletion闭包内部我们检查完成是否有错误错误 ,然后使用.failure选项传递相应的promise错误。



请注意,这里我们收集了“发布者链”先前阶段中“抛出”的所有错误。

接下来,我们在Set <AnyCancellable>()属性中存储“ subscription” 订阅
事实是,“订阅” 订阅可取消的 ,它是一种协议,在fetchMovies函数完成后会破坏并清除所有内容。 为了确保即使完成此功能后仍保留“订阅” 订阅 ,我们需要记住fetchMovies函数外部变量中的“订阅” 订阅 。 在我们的示例中,我们使用subscriptions属性,该属性具有Set <AnyCancellable>()类型,并使用.store(in:&self.subscriptions)方法 ,该方法可确保fetchMovies函数完成工作后“预订”的可用性



到此,就结束了使用Combine框架从tmdb数据库中选择电影的fetchMovies方法的形成。 作为输入参数的fetchMovies方法采用枚举Endpoint枚举的值,即我们感兴趣的特定影片:

.nowPlaying-当前正在播放的电影,
.upcoming-即将上映的电影,
.popular-热门电影,
.topRated-顶级电影,即具有很高的评价。
让我们尝试以Table View Controller的形式使用常见的UIKit用户界面将此API应用于应用程序设计:



以及使用新的声明性SwiftUI框架构建用户界面的应用程序:



“订阅”来自常规View Controller 电影的电影


我们将移动到MovieListViewController.swift文件,并在viewDidLoad方法中调用fetchMovies方法。



在我们的fetchMovies方法中我们使用较早开发的movieAPI及其fetchMovies方法,其中.nowPlaying参数作为from输入参数的端点 。 也就是说,我们将选择电影院屏幕上当前正在播放的电影。



movieAPI.fetchMovies(来自:.nowPlaying)方法返回Future “发布者”,我们使用接收向其“订阅”,并为其提供两个闭包。 在receiveCompletion闭包中, 我们检查是否存在错误错误,并通过显示的错误消息向用户警报显示紧急警告。



receiveValue闭包中我们调用generateSnapshot方法并将选定的电影传递给它



generateSnapshot函数使用我们的电影 生成一个新的NSDiffableDataSourceSnapshot ,并将生成的快照应用于表的diffableDataSource

我们启动该应用程序,并观察UIKit如何与Combine框架中的“发布者”和“订阅者”一起工作。 这是一个非常简单的应用程序,不允许您调入各种电影集-现在在屏幕上显示,流行,高评级或在不久的将来将出现在屏幕上的电影。 我们仅看到将要出现在屏幕上的电影( .upcoming )。 当然,您可以通过添加任何UI元素来设置Endpoint枚举的值来实现此目的,例如使用StepperSegmented Control ,然后更新用户界面。 这是众所周知的,但是我们不会在基于UIKit应用程序中执行此操作,而是将其留给新的声明性SwiftUI框架。
可在Github上的CombineFetchAPICompleted-UIKit找到基于CombineFetchAPICompleted-UIKit应用程序的代码。

使用SwiftUI显示电影电影


CombineFetchAPI-MY SwiftUI File -> New -> Project Single View App iOS :



UISwiftUI :



Movie.swift Model , TMDb MovieStore.swift , MovieStoreAPIError.swift MovieService.swift , MovieService Protocol :



SwiftUI , Codable , JSON , 可识别,如果我们想使自己更容易将电影列表[Movie]显示为列表ListSwiftUI中的模型不需要是EquatableHashable的,这是UIKit API以前的UIKit应用程序中UITableViewDiffableDataSource所要求的。因此,我们从< struct Movie结构中删除了所有与EquatableHashable协议相关的方法............................有一篇很棒的Identifiable文章,显示了协议之间的区别和相似之处





Swift可识别的,可哈希的平等的

我们将使用SwiftUI创建的用户界面基于以下事实:在获取数据并获取所需的影片收藏时,我们会更改端点,并以列表的形式显示:



UIKit一样,使用movieAPI函数对数据进行采样.fetchMovies(来自端点:Endpoint),它将获取所需的端点并返回“发布者” Future <[Movie,MovieStoreAPIError]>。如果我们看一下Endpoint枚举,将会看到我们可以初始化所需的选项Endpoint枚举的情况因此使用索引索引



来获得所需的电影集因此,要获取我们需要的电影的电影集,只需设置Endpoint枚举的相应索引indexEndpoint即可。让我们开始吧,在我们的例子中,它将是实现ObservableObject协议MoviesViewModel。在我们的项目中为我们添加一个新的MoviesViewModel.swift文件在这个非常简单的类中,我们有两个@Published属性:一个@Published var indexEndpoint:IntView ModelView Model



-输入,其他@Published var电影:[电影] -输出。一旦我们把@Published前财产indexEndpoint,我们就可以开始使用它作为一个简单的属性indexEndpoint,作为发布商,$ indexEndpoint
在初始化MoviesViewModel类的实例时我们必须将链从输入“ publisher” $ indexEndpoint延伸到输出“ publisher” TYPE AnyPublisher <[Movie],Never>,使用我们已知的movieAPI.fetchMovies(from:Endpoint(index)函数):indexPoint))flatMap运算符



接下来,我们使用非常简单的“订阅者” 分配(到:\。movies,on:self)来“订阅”这个新接收的“发布者” 并将从“发布者”接收的值分配给电影输出数组仅当“发布者”没有引发错误,即错误类型为Never时我们才可以应用“订阅” 分配(应用于:\ .movi​​es,on:self)。如何实现呢?与操作员的帮助replaceError(附:[])将取代的膜的空阵列上的任何错误的电影也就是说,我们的SwiftUI应用程序的第一个简单版本将不会向用户显示有关可能的错误的信息。



, View Model , UI . ContentView.swift View Model @EnvironmentObject var moviesViewModel Text(«Hello, World!»)
Text("\(moviesViewModel.indexEndpoint)") , indexEndpoint .



View Model indexEndpoint = 2 , , ( Upcoming ):



UI , . Stepper :



Picker :



«» $moviesViewModel.indexEndpoint View Model , ( ) :



List ForEach movie :



moviesViewModel.movies View Model :



«» $moviesViewModel.movies $,因为我们不会编辑此电影列表中的任何内容。我们使用通常的movieViewModel.movi​​es属性

您可以通过在列表的每一行中显示相应电影的电影海报来使电影列表更有趣,该电影海报URL显示在Movie结构中



我们从Thomas Ricouard的美丽项目MovieSwiftUI中借用了这个机会

与加载影片的情况一样,对于UIImage图像,我们拥有ImageService服务,该服务通过Combine实现fetchImage方法返回“ publisher” AnyPublisher <UIImage?,Never>



...和最终类ImageLoader:ObservableObject该类使用@Published属性图像实现了ObservableObject协议:UIImage?



ObservableObject协议提出的唯一要求是存在objectWillChange属性SwiftUI使用此属性来了解此类实例中的某些更改,并且一旦发生这种情况,将更新所有依赖于此类实例的View。通常,编译器会自动创建一个objectWillChange属性,并且所有@Published属性也会自动通知它。在某些特殊情况下,您可以手动创建一个objectWillChange并将更改通知它。我们有这种情况。
ImageLoader类中 @Published var image:UIImage? 。 , ImageLoader , «» $image «» loadImage() , poster size @Published var image:UIImage?我们将这些更改通知给objectWillChange
表中可能有很多这样的图像,这会导致大量的时间开销,因此我们使用ImageLoaderimageLoader实例缓存我们有一个特殊的View来播放MoviePosterImage电影海报...,并且在主ContentView中显示电影列表时将使用它可以在Github上的文件夹中找到基于SwiftUI应用程序的代码,不显示错误













CombineFetchAPI-NOError

远程异步电影获取的显示错误。


到目前为止,我们还没有使用或显示从TMDb网站远程异步选择电影期间发生的错误。尽管我们使用的函数movieAPI.fetchMovies(来自端点:Endpoint)允许我们执行此操作,因为它返回“发布者” Future <[Movie,MovieStoreAPIError]>

为了考虑错误,我们将View Model另一个@Published属性movieError添加到了:MovieStoreAPIError?代表错误。这是一个Optional属性,其初始值为nil,它对应于没有错误:



为了获得此错误,moviesError , MoviesViewModel «» sink :



moviesError UI , nil



AlertView :



, API :



SwiftUI Github CombineFetchAPI-Error .

, Future<[Movie],MovieStoreAPIError> , AnyPublisher<[Movie], Never> fetchMoviesLight :



( Never ) «» assign(to: \.movies, on: self) :



:



结论


使用Combine框架处理在时间上异步出现的一系列非常简单。Combine提供的操作员功能强大且灵活。结合使我们能够避免通过使用上游链的编写复杂异步代码“出版商» 出版商,利用运营商和内置的“用户» 认购Combine在比Foundation低的级别上构建,在许多情况下不需要Foundation并具有惊人的性能。



SwiftUI也与Combine紧密相关<由于其@ObservableObject@Binding@EnvironmentObject
iOS开发人员一直在等待Apple这种官方框架,终于在今年实现了。

链接:

使用Apple Combine Framework获取远程异步API
尝试! Swift NYC 2019-合并入门
“ Swift中的终极合并框架教程”。

合并:结合Swift进行异步编程

-WWDC 2019-视频-Apple Developer。会议722
(会议722的简介,俄语的介绍)

在实践中结合-WWDC 2019-视频-Apple Developer。会议721
(第721节“俄语的实用用法”摘要)

SwiftUI&Combine:在一起更好。为什么SwiftUI和Combine帮助您创建更好的应用程序。

MovieSwiftUI

可视化SwiftUI结合魔术(1)
可视化SwiftUI结合魔术-第2部分(在Combine中操作,订阅和取消操作)
Visualize Combine Magic with SwiftUI Part 3 (See Combine Merge and Append in Action)
Visualize Combine Magic with SwiftUI — Part 4

Visualize Combine Magic with SwiftUI — Part 5

Getting Started With the Combine Framework in Swift
Transforming Operators in Swift Combine Framework: Map vs FlatMap vs SwitchToLatest
Combine's Future
Using Combine
URLSession and the Combine framework

Source: https://habr.com/ru/post/zh-CN476678/


All Articles