Combine是一个功能性的响应式
Swift
框架,最近已在所有
Apple
平台(包括
Xcode 11
。 使用
合并,很容易处理随时间推移异步出现的值序列。 通过放弃委托和复杂的嵌套
回调 ,它还简化了异步代码。
但是,一开始学习
Combine似乎并不那么简单。 事实是,
合并的主要“参与者”是诸如“
发布者”,“
发布者”,“订阅者”,
订阅者和操作
员 “操作
员 ”之类的抽象概念,没有它们,就不可能进一步理解
合并功能的逻辑。 但是,由于
Apple
为开发人员提供了现成的“发布者”,“订阅者”和操作员,因此使用
Combine编写的代码非常紧凑且易于阅读。
您将在一个与
现在从非常流行的
TMDb数据库中异步
检索电影信息有关的应用程序示例中看到这一点。 我们将创建两个不同的应用程序:
UIKit和
SwiftUI ,并展示
Combine如何与它们一起工作。

希望本文使您能够更轻松地学习
Combine 。 在
Github上可以找到本文开发的所有应用程序的代码。
合并有几个主要组成部分:
发布者Publisher 。

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


Apple
为开发人员提供了现成的“发布者”的特定实现:
Just ,
Future ,
Empty ,
Deferred ,
Sequence ,
@Published等。 “发布者”也被添加到
Foundation类中:
URLSession ,<
NotificationCenter ,
Timer 。
“订户” 订户 。

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

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

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

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

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

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

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

“主题” 主题 。

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

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

您会在此处看到许多熟悉的运算符名称:
compactMap ,
map ,
filter ,
dropFirst ,
append 。
基金会发行人基金会中内置了发行人。
Apple
还为开发人员提供了
Foundation <framework(即发布者的发布者)中一些已经内置的
Combine功能,用于执行以下任务:使用
URLSession提取数据,使用
Notification ,
Timer处理通知以及基于
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
检索错误urlError , encodingError 解码错误和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 ,它是一个介于
0到
100之间的随机整数
Int值。 接下来,我们使用两个
将来的订阅(
cancellable和
cancellable1) ,尽管内部生成一个随机数,但两者都给出相同的结果。
1.
应注意,与其他“发布者”相比,“ 合并中的未来 ”的“发布者”具有某些行为特征:
- “发布者” Future总是“发布”一个值( 值或错误),从而完成了其工作。
- 与其他主要是struct (
value 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 <和cancellable1或Set <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
代码,以确保其值在
200到
300之间。 如果不是,则抛出
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枚举的值来实现此目的,例如使用
Stepper
或
Segmented Control
,然后更新用户界面。 这是众所周知的,但是我们不会在基于
UIKit的
应用程序中执行此操作,而是将其留给新的声明性
SwiftUI框架。
可在
Github上的
CombineFetchAPICompleted-UIKit
找到基于
CombineFetchAPICompleted-UIKit
的
应用程序的代码。
使用SwiftUI显示电影电影
。
CombineFetchAPI-MY
SwiftUI File
->
New
->
Project
Single View App
iOS
:

UI
—
SwiftUI :

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

Swift
可识别的,可哈希的和平等的。我们将使用SwiftUI创建的用户界面基于以下事实:在获取数据并获取所需的影片收藏时,我们会更改端点,并以列表的形式显示:
与UIKit一样,使用movieAPI函数对数据进行采样.fetchMovies(来自端点:Endpoint),它将获取所需的端点并返回“发布者” Future <[Movie,MovieStoreAPIError]>。如果我们看一下Endpoint枚举,将会看到我们可以初始化所需的选项Endpoint枚举的情况,并因此使用索引索引
来获得所需的电影集:因此,要获取我们需要的电影的电影集,只需设置Endpoint枚举的相应索引indexEndpoint即可。让我们开始吧,在我们的例子中,它将是实现ObservableObject协议的MoviesViewModel类。在我们的项目中为我们添加一个新的MoviesViewModel.swift文件:在这个非常简单的类中,我们有两个@Published属性:一个@Published var indexEndpoint:IntView Model
View 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时,我们才可以应用“订阅” 分配(应用于:\ .movies,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.movies属性。您可以通过在列表的每一行中显示相应电影的电影海报来使电影列表更有趣,该电影海报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。表中可能有很多这样的图像,这会导致大量的时间开销,因此我们使用ImageLoader类的imageLoader实例的缓存:我们有一个特殊的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 4Visualize Combine Magic with SwiftUI — Part 5Getting Started With the Combine Framework in SwiftTransforming Operators in Swift Combine Framework: Map vs FlatMap vs SwitchToLatestCombine's FutureUsing CombineURLSession and the Combine framework