查询HTTP
是开发iOS
应用程序时需要获得的最重要的技能之一。在早期版本Swift
(版本5之前)中,无论您是从头开始生成这些请求还是使用知名的Alamofire框架,最终都会产生来自该callback
类型的 复杂代码completionHandler: @escaping(Result<T, APIError>) -> Void
。功能反应式编程在新框架中的外观与现有结合在一起,为您提供了独立编写非常紧凑的代码以从Internet提取数据所需的所有工具。
在本文中,按照概念,我们将创建“发布者”Swift 5
Combine
URLSession
Codable
Combine
Publisher
可以从Internet中选择数据,将来我们可以轻松地“订阅” UI
这些数据, UIKit
并在有帮助的情况下进行 设计时使用SwiftUI
。由于 SwiftUI
它看起来更简洁,更有效,因为“出版商”的作用 Publisher
并不限于样本数据,并进一步延伸到用户界面控件(UI
)。事实是,使用具有属性的类SwiftUI
来View
执行数据分离 ObservableObject
,@Published
该类的更改会 SwiftUI
自动监控并完全重绘View
。在这些 ObservableObject
类中,您可以非常简单地放置应用程序的某些业务逻辑,如果其中一些@Published
属性是同步和/或异步变换其他的结果@Published
可以直接改变所述用户接口的这样的“活性”的元素(属性UI
),为文本框TextField
,Picker
,Stepper
,Toggle
等。为了清楚说明问题所在,我将举一些具体例子。现在,NewsAPI.org 和Hacker News之类的许多服务都提供新闻聚合器,以 使用户可以根据他们的兴趣选择不同的文章集。对于NewsAPI.org新闻 聚合器,它可以是最新新闻,也可以是某些类别的新闻-“体育”,“健康”,“科学”,“技术”,“商业”或特定信息源“ CNN”的新闻,ABC新闻,彭博社等用户通常以Endpoint
形成对他必要的形式的形式来“表达”他对服务的期望URL
。因此,使用该框架 Combine
,您可以ObservableObject
类使用非常紧凑的代码(在大多数情况下不超过10-12行)一次以Endpoint
“被动” @Published
属性到“主动” @Published
属性的“订阅”形式形成对商品列表的同步和/或异步依赖 。该“订阅”将在ObservableObject
类实例的整个“生命周期”内有效 。然后,SwiftUI
您将使用户有机会仅管理@Published
表单中的“活动” 属性Endpoint
,即他希望看到的内容:是最新消息的文章还是“健康”部分的文章。UI
这些ObservableObject
类及其“被动” @Published属性将自动提供 文章本身的外观以及“健康”部分中的最新新闻或文章。在代码中SwiftUI
您将不需要显式请求选择文章,因为ObservableObject
发挥作用的类负责它们在屏幕上的正确和同步显示 View Model
。在一系列文章中,我将向您展示如何与 NewsAPI.org 和Hacker News以及TMDb电影数据库一起使用。在这三种情况下,大约都可以使用相同的使用模式 Combine
,因为在这种应用程序中,您始终必须创建电影或文章的清单,选择它们随附的“照片”(图像),然后使用搜索栏搜索所需电影或文章的数据库。访问此类服务时,例如由于您指定了错误的密钥API-key
或超出了允许的请求数量或其他原因,可能会发生错误。您需要处理此类错误,否则您将冒着使用户完全陷入空白屏幕的风险。因此,您不仅需要能够使用 Combine
从Internet中选择数据,而且还需要报告采样期间可能发生的错误,并控制它们在屏幕上的显示。我们将通过开发与NewsAPI.org新闻聚合器交互的应用程序来开始制定策略 。我必须说,在此应用程序中,SwiftUI
它将最小程度地使用它,而没有任何多余的装饰,仅仅是为了说明如何Combine
与“发布者”一起使用Publisher
和“订阅” Subscription
受到影响UI
。建议您在NewsAPI.org网站上注册并接收API
完成对NewsAPI.org服务的所有请求所需的密钥。您必须将其放置在结构中的NewsAPI.swift文件中APIConstants
。本文的应用程序代码在Github上。NewsAPI.org
服务 使您可以选择有关当前新闻文章 [Article]
及其来源的信息 [Source]
。我们的数据模型将非常简单,它位于Articles.swift文件中 :import Foundation
struct NewsResponse: Codable {
let status: String?
let totalResults: Int?
let articles: [Article]
}
struct Article: Codable, Identifiable {
let id = UUID()
let title: String
let description: String?
let author: String?
let urlToImage: String?
let publishedAt: Date?
let source: Source
}
struct SourcesResponse: Codable {
let status: String
let sources: [Source]
}
struct Source: Codable,Identifiable {
let id: String?
let name: String?
let description: String?
let country: String?
let category: String?
let url: String?
}
该文章Article
将包含标识符id
,标题 title
,描述 description
,作者author
,“图片”的URL urlToImage
,出版日期和出版物publishedAt
来源source
。在文章上方[Article]
是一个加载项,NewsResponse
在该加载项中,我们仅对property属性感兴趣,该属性articles
是一系列文章。根结构NewsResponse
和结构 Article
是Codable
,这将使我们能够从字面上看两行代码,将JSON
数据解码为模型。如果我们想使自己更轻松地将一组文章显示为列表 中的列表 ,则结构 Article
也应该是Identifiable
这样。协议要求存在一个属性[Article]
List
SwiftUI
Identifiable
id
我们将提供一个人工的唯一标识符UUID()
。信息来源 Source
将包含标识符 id
,名称 name
,描述 description
,国家 country
,出版物来源类别category
,站点URL url
。在信息源上方 [Source]
有一个加载项,SourcesResponse
在该加载项 中,我们仅对一个属性感兴趣,该属性sources
是一组信息源。根结构SourcesResponse
和结构 Source
是Codable
,这将使我们能够非常轻松地将 JSON
数据解码为模型。该结构 Source
也应该是Identifiable
,如果我们要促进信息源阵列的显示 [Source]
在列表的形式 List
在SwiftUI
。该协议Identifiable
要求存在id
我们已经拥有的属性,因此不需要我们付出额外的努力。现在考虑我们 API
对NewsAPI.org服务的需求 ,并将其放置在NewsAPI.swift文件中 。我们的中心部分API
是一个类NewsAPI
,提供了两种从NewsAPI.org新闻聚合器中选择数据的方法 -文章 [Article]
和信息来源 [Source]
:fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
- [Article]
根据参数选择文章 endpoint
,fetchSources (for country: String) -> AnyPublisher<[Source], Never>
- [Source]
为特定国家/地区选择信息来源 country
。
这些方法不仅返回文章数组 [Article]
或信息源数组 [Source]
,而且还返回Publisher
新框架的相应“发布者” Combine
。两家出版商均未返回任何错误- Never
并且如果仍然发生抽样或编码错误,则将返回空的文章[Article]()
或信息源 数组, [Source]()
而没有任何消息说明这些数组为空。 我们希望从NewsAPI.org服务器中选择哪些文章或信息源,我们将使用枚举进行指示enum Endpoint:
enum Endpoint {
case topHeadLines
case articlesFromCategory(_ category: String)
case articlesFromSource(_ source: String)
case search (searchFilter: String)
case sources (country: String)
var baseURL:URL {URL(string: "https://newsapi.org/v2/")!}
func path() -> String {
switch self {
case .topHeadLines, .articlesFromCategory:
return "top-headlines"
case .search,.articlesFromSource:
return "everything"
case .sources:
return "sources"
}
}
}
它:- 最新消息
.topHeadLines
, - 某一类别的新闻(体育,健康,科学,商业,技术)
.articlesFromCategory(_ category: String)
, - 来自特定信息来源的新闻(CNN,ABC新闻,Fox新闻等)
.articlesFromSource(_ source: String)
, - 任何
.search (searchFilter: String)
符合特定条件的新闻 searchFilter
, .sources (country:String)
特定国家的信息来源country
。
为了方便我们需要的选项的初始化,我们根据索引和字符串对各种文章和信息源列表的枚举添加了一个Endpoint
初始化器,这对于不同的枚举选项具有不同的含义:init?
index
text
init? (index: Int, text: String = "sports") {
switch index {
case 0: self = .topHeadLines
case 1: self = .search(searchFilter: text)
case 2: self = .articlesFromCategory(text)
case 3: self = .articlesFromSource(text)
case 4: self = .sources (country: text)
default: return nil
}
}
让我们回到该类 NewsAPI
并更详细地考虑第一种方法fetchArticles (from endpoint: Endpoint)-> AnyPublisher<[Article], Never>
,该方法 [Article]
根据参数选择文章endpoint
,并且不返回任何错误- Never
:func fetchArticles(from endpoint: Endpoint) -> AnyPublisher<[Article], Never> {
guard let url = endpoint.absoluteURL else {
return Just([Article]()).eraseToAnyPublisher()
}
return
URLSession.shared.dataTaskPublisher(for:url)
.map{$0.data}
.decode(type: NewsResponse.self,
decoder: APIConstants .jsonDecoder)
.map{$0.articles}
.replaceError(with: [])
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
- 根据请求的
endpoint
形式URL
获得所需的文章列表endpoint.absoluteURL
,如果无法完成,则返回一个空的文章数组[Article]()
- «»
dataTaskPublisher(for:)
, Output
(data: Data, response: URLResponse)
, Failure
- URLError
, map { }
(data: Data, response: URLResponse)
data
, JSON
data
, NewsResponse
, articles: [Atricle]
map { }
- articles
, - -
[ ]
, main
, UI
,- «» «»
eraseToAnyPublisher()
AnyPublisher
.
选择信息源的任务已分配给第二种方法- fetchSources (for country: String) -> AnyPublisher<[Source], Never>
这是第一种方法的精确语义副本,只是这次[Article]
我们将选择信息源而不是文章[Source]
:func fetchSources() -> AnyPublisher<[Source], Never> {
guard let url = Endpoint.sources.absoluteURL else {
return Just([Source]()).eraseToAnyPublisher()
}
return
URLSession.shared.dataTaskPublisher(for:url)
.map{$0.data}
.decode(type: SourcesResponse.self,
decoder: APIConstants .jsonDecoder)
.map{$0.sources}
.replaceError(with: [])
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
它以AnyPublisher <[Source], Never>
一个信息源数组的形式返回值的“发布者” ,[Source]
并且没有错误 Never
(如果有错误,则返回一个空的源数组 [ ]
)。我们将挑选出这两种方法的共同部分,安排它作为一个Generic
函数fetch(_ url: URL) -> AnyPublisher<T, Error>
返回的 Generic
“发行” AnyPublisher<T, Error>
基础上URL
:
func fetch<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data}
.decode(type: T.self, decoder: APIConstants.jsonDecoder)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
这将简化前两种方法:
func fetchArticles(from endpoint: Endpoint)
-> AnyPublisher<[Article], Never> {
guard let url = endpoint.absoluteURL else {
return Just([Article]()).eraseToAnyPublisher()
}
return fetch(url)
.map { (response: NewsResponse) -> [Article] in
return response.articles }
.replaceError(with: [Article]())
.eraseToAnyPublisher()
}
func fetchSources(for country: String)
-> AnyPublisher<[Source], Never> {
guard let url = Endpoint.sources(country: country).absoluteURL
else {
return Just([Source]()).eraseToAnyPublisher()
}
return fetch(url)
.map { (response: SourcesResponse) -> [Source] in
response.sources }
.replaceError(with: [Source]())
.eraseToAnyPublisher()
}
这样获得的“发布者”在有人“订阅”之前不会交付任何东西。我们可以在设计时做到这一点UI
。“出版商” Publisher
作为View Model
在 SwiftUI
。文章清单。
现在介绍一下运作的逻辑SwiftUI
。 他们所响应的“外部变化”的SwiftUI
唯一抽象View
是“发布者” Publisher
。 “外部更改”可以理解为计时器 Timer
,NotificationCenter
或带有Model对象的通知,使用协议ObservableObject
可以将其转换为外部单个“真理来源”(真理来源)。 对于普通的“发布者”类型Timer
或NotificationCenter
View
使用方法进行反应onReceive (_: perform:)
。Timer
我们稍后将在关于Hacker News的应用程序的第三篇文章中介绍使用“发布者”的示例。在本文中,我们将重点介绍如何为 SwiftUI
外部“真理之源”(真理之源)。首先,让我们看一下在SwiftUI
显示各种类型文章的特定示例中,最终的“发布者”应如何发挥作用:.topHeadLines
-最新新闻, .articlesFromCategory(_ category: String)
-特定类别的 .articlesFromSource(_ source: String)
新闻,-特定信息源的新闻,.search (searchFilter: String)
-根据特定条件选择的新闻。
根据 Endpoint
选择的用户,我们需要更新articles
从NewsAPI.org选择的文章列表。为此,我们将创建一个非常简单的类 ArticlesViewModel
,该类实现ObservableObject
具有三个 @Published
属性的协议: 
@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , ( «», View
TextField
),@Published var articles: [Article]
- ( «», NewsAPI.org, «»).
一旦设置了 @Published
属性indexEndpoint
或 searchString
,我们就可以将它们同时用作简单属性 indexEndpoint
和 searchString
,以及作为“发布者” $indexEndpoint
和 $searchString
。在一个类中 ArticlesViewModel
,您不仅可以声明我们感兴趣的属性,还可以规定它们交互的业务逻辑。为此,在初始化类的实例时 ArticlesViewModel
,init
我们可以创建一个“订阅”,该“订阅”将在该类实例的整个“生命周期”中起作用, ArticlesViewModel
并重现文章列表articles
对索引 indexEndpoint
和搜索字符串的依赖性searchString
。为此,Combine
我们扩展了“发布者”链$indexEndpoint
并$searchString
输出“发布者”AnyPublisher<[Article], Never>
其值是文章列表 articles
。然后,我们使用运算符“订阅”它,assign (to: \.articles, on: self)
并获得所需的文章列表articles
作为@Published
定义的“输出” 属性UI
。我们必须从性质拉动链条不要直接 indexEndpoint
和searchString
,即从“出版商” $indexEndpoint
和$searchString
谁参与创作UI
的帮助下SwiftUI
,我们将有使用用户界面元素更改它们 Picker
和TextField
。我们将如何做?我们fetchArticles (from: Endpoint)
的类库中 已经有一个函数,该函数根据值NewsAPI
返回“发布者”AnyPublisher<[Article], Never>
Endpoint
,而我们只能以某种方式使用“发布者”的值 $indexEndpoint
并将 $searchString
其转换endpoint
为该函数的参数。 首先,将“发布者” $indexEndpoint
和 结合在一起 $searchString
。为此,该Combine
操作员存在Publishers.CombineLatest
:
要基于从先前“发布者”接收到的数据创建一个新的“发布Combine
者” ,请使用该操作符 flatMap
:
接下来,我们使用非常简单的“订阅者”“订阅”该新接收的“发布者”, assign (to: \.articles, on: self)
并从“由于@Published
数组的 值是发布者“的值 articles
:
我们刚刚创建了一个init( )
异步的发布者”,并“订阅了”AnyCancellable
“订阅”,这很容易验证我们是否将我们的“订阅”保持为常数let subscription
:“订阅”
的主要属性AnyCancellable
是,一旦它离开范围,它所占用的内存就会自动释放。因此,一旦init( )
完成,该“预订”将被删除ARC
,而没有时间将接收到的具有时延的异步信息分配给阵列articles
。从字面上看,异步信息根本就无处“着陆”,“地球已经从它的脚下掉下来了”。为了保存这样的“订阅”,有必要创建一个超出初始化程序的 init()
变量,该变量将在类实例的整个“生命周期” var cancellableSet
中将我们的AnyCancellable
“订阅” 保存 在该变量中 ArticlesViewMode
。 因此,我们删除了常数let subscription
,并记住我们的AnyCancellable
“订阅”中的变量 cancellableSet
使用操作.store ( in: &self.cancellableSet)
:
“订阅”异步“发行人”,我们创造了init( )
整个类实例的整个“生命周期”将被保留 ArticlesViewModel
。我们可以任意更改``发布者'' $indexEndpoint
和/或 的含义 searchString
,并且始终要感谢创建的``订阅'',我们将articles
无需花费额外的精力就可以拥有与这两个发布者的价值相对应的一系列文章 。这个 ObservableObject
类通常被称为 View Model
。为了减少键入搜索字符串时服务器的调用次数searchString
,我们不应该使用搜索字符串本身的“发布者” $searchString
,以及它的修改版本validString
:
现在,我们已经View Model
准备好撰写文章,让我们开始创建用户界面(UI
)。为了与 模型SwiftUI
同步,View
使用了ObservableObject
一个@ObservedObject
变量,该变量引用此模型的类的实例。这是对- ObservableObject
类和@ObservedObject
引用该类实例的 变量-控制in中用户界面(UI
) 的更改SwiftUI
。我们以变量的形式将结构的ContentView
实例添加到结构ArticleViewModel
中,var articleViewModel
并替换Text ("Hello, World!")
为文章列表,ArticlesList
在其中放置articlesViewModel.articles
从我们的文章中获得 的文章 View Model
。结果,我们获得了固定索引和默认索引的文章列表 indexEndpoint = 0
,即.topHeadLines
最新新闻:在
屏幕上添加一个 UI
元素,以控制要显示的文章集。我们将使用 Picker
索引更改$articlesViewModel.indexEndpoint
。符号的存在是 $
强制性的,因为这意味着@Published
“发布者” 提供的值将发生更改 。该“发布者”的“订阅”会立即触发,我们在此启动init ()
,“输出” @Published
“发布者” articles
将更改,并且我们将在屏幕上看到不同的文章列表:
这样,我们可以接收所有三个选项的文章数组-“ topHeadLines”,“搜索” “和”来自类别“:
...,但使用固定和默认的搜索字符串searchString = "sports"
(需要时):
但是,对于此选项, "search"
您必须为用户提供SearchView
用于输入搜索字符串的文本字段:
结果,用户可以通过键入的搜索字符串来搜索任何新闻:
对于该选项, "from category"
有必要向用户提供有机会选择一个类别,我们开始与类别science
:
作为一个结果,用户可以搜索任何新闻对新闻选择的类别- ,science
, health
, :business
我们可以看到一个非常简单的 模型,有两个用户控制功能- 和technology

ObservableObject
@Published
indexEndpoint
searchString
-使您可以从NewsAPI.org网站中选择各种信息 。信息来源清单
让我们看一下SwiftUI
NewsAPI类中接收到的信息源的“发布者”将如何运行fetchSources (for country: String) -> AnyPublisher<[Source], Never>
。我们将获得不同国家/地区的信息资源列表:
...以及按名称搜索信息的能力:
...以及有关选定来源的详细信息:其名称,类别,国家/地区,简短说明和网站链接:
如果单击链接,我们将转到此网站信息来源。为了使所有这些都能起作用,您需要一个非常简单的 ObservableObject
Model,该Model仅具有两个用户控制的@Published
属性- searchString
和 country
:
同样,我们使用相同的方案:在初始化类的SourcesViewModel
类实例时, init
我们创建了一个“订阅”,该订阅将在类实例的整个“生命周期”中运行, SourcesViewModel
并确保信息源列表取决于 sources
国家 country
和搜索字符串 searchString
。在帮助下, Combine
我们将链从“发布者”中拉出,$searchString
并$country
输出“发布者” AnyPublisher<[Source], Never>
,其值是信息源列表。我们使用运算符“订阅”它assign (to: \.sources, on: self)
,得到所需信息源的列表 sources
。并使用运算符将收到的AnyCancellable
“订阅” 记住 在变量中 。
现在我们有了信息源,让我们开始创建。 B 同步ccancellableSet
.store ( in: &self.cancellableSet)
View Model
UI
SwiftUI
View
ObservableObject
该模型使用一个@ObservedObject
变量,该变量引用此Model类的实例。将ContentViewSources
类实例以SourcesViewModel
变量的形式添加到结构 中var sourcesViewModel
,删除 Text ("Hello, World!")
并放置View
3个@Published
属性中的每个 属性 sourcesViewModel
: SearchView
搜索栏的 文本框 searchString
,-
Picker
为了国家country
, SourcesList
信息来源清单
结果,我们得到了所需的信息View
:
在此屏幕上,我们仅使用文本框管理搜索字符串,并使用来管理SearchView
“国家/地区” Picker
,其余的将自动进行。信息源列表仅SourcesList
包含有关每个源的最少信息-名称source.name
和简短说明source.description
:
... ...但是,您可以使用以下链接获得有关所选源的更详细的信息:在该链接NavigationLink
中,destination
我们指示 DetailSourceView
哪些源数据是信息源 source
以及所需的类实例ArticlesViewModel
,得到他的文章清单articles
:
看看我们如何在信息来源列表中获得选定信息来源的文章列表 SourcesList
。我们的老朋友对我们有帮助- ArticlesViewModel
我们必须为这两个类 设置“输入” @Published
属性:- index
indexEndpoint = 3
,即.articlesFromSource (_source:String)
对应于固定来源文章选择的选项 source
, - 字符串
searchString
作为源本身(或更确切地说是其标识符)source.id
:
通常,如果您查看整个NewsApp应用程序,则不会看到我们明确要求NewsNews.org网站上提供文章或信息来源的任何地方 。我们仅管理 @Published
数据,但会View Model
做我们的工作:选择所需的文章和信息来源。下载UIImage
文章图片Article
。
本文的模型 Article
包含一个URL
附带的图像 urlToImage
:
基于此, URL
将来我们必须UIImage
从NewsAPI.org网站上 获取图像本身。我们已经熟悉此任务。在该类中ImageLoader
,使用函数fetchImage(for url: URL?) -> AnyPublisher<UIImage?, Never>
创建一个AnyPublisher<UIImage?, Never>
具有图像值 UIImage?
且没有错误 的“发布者” Never
(实际上,如果发生错误,则返回图像 nil
)。UIImage?
在设计用户界面(UI
)时,您可以“订阅”该“发布者”以接收图像 。该函数的源数据fetchImage(for url: URL?)
是 url
,我们有:
如果我们知道,
让我们详细考虑在 Combine
“发布者” 的帮助下如何进行:AnyPublisher <UIImage?, Never>
url
- 如果
url
相等nil
,返回Just(nil)
, - 基于
url
“发布者” 的形式dataTaskPublisher(for:)
,其输出值为Output
元组(data: Data, response: URLResponse)
和错误 Failure
- URLError
, - 我们采取仅数据
map {}
从元组(data: Data, response: URLResponse)
以供进一步处理 data
,和形式UIImage
, - 如果前面的步骤返回错误
nil
, - 我们将结果传递到
main
流中,因为我们假定在设计中进一步使用它UI
, - “擦除”“发布者”的类型并返回副本
AnyPublisher
。
您会看到代码非常紧凑且可读性强,没有任何代码callbacks
。让我们开始View Model
为图像创建 UIImage?
。这是一个ImageLoader
实现protocol 的类ObservableObject
,具有两个 @Published
属性:@Published url: URL?
是URL
图像@Published var image: UIImage?
是来自NewsAPI.org的图片本身:
再一次,初始化类的实例时, ImageLoader
我们必须将链从输入“ publisher” $url
延伸到输出“ publisher” AnyPublisher<UIImage?, Never>
,稍后将 “订阅” 到输出并获得所需的图像image
:
我们使用运算符 flatMap
和一个非常简单的“ subscriber” assign (to: \image, on: self)
将其分配给来自“ publisher”的接收者 ``属性值@Published image
:
再次使用操作符cancellableSet
存储在变量 AnyCancellable
''预订''中 store(in: &self.cancellableSet)
。此“图像下载器”的逻辑是,您从 nil URL
没有预加载的图像之外的其他位置下载图像,即image == nil
。如果在下载过程中检测到任何错误,则图像将不存在,即image
它将保持相等nil
。在SwiftUI
我们的帮助ArticleImage
下,imageLoader
该类的实例使用此图像来显示图像ImageLoader
。如果他的图像图像不相等nil
,则使用来显示 Image (...)
,但如果相等nil
,则根据图像的相等性,不显示url
任何内容EmptyView()
,或者显示Rectangle
带有旋转文本T 的矩形ext("Loading...")
:
此逻辑适用于这种情况当您肯定知道的时候( url
除了 nil
获得图片)image
,电影电影数据库就是这种情况。 TMDb。使用NewsAPI.org,新闻聚合器 就不同了。某些信息来源的文章与nil URL
图像提供了不同的信息 ,但是对它的访问是封闭的,我们得到了一个Rectangle
带有旋转文本的矩形,该矩形Text("Loading...")
将永远不会被替换:
在这种情况下,如果 URL
图像不同于 nil
,则nil
图像 的相等性 image
可能意味着图像正在加载以及加载时发生错误的事实,我们将永远不会得到图片image
。为了区分这两种情况,我们向该类中ImageLoader
的两个现有@Published
属性添加了 一个: @Published var noData = false
-这是一个布尔值,通过它我们将指示由于选择过程中的错误而导致图像数据不存在的情况:
创建“订阅”时,我们将 init
捕获 Error
加载图像时发生的所有错误并在@Published
属性中累积它们的存在 self.noData = true
。如果下载成功,则获得图像image
。
我们 基于以下 函数创建“发布者” :我们通过初始化“发布者” 开始创建方法,该方法可用于使用闭包异步获取单个TYPE值。闭包有一个参数- 这是TYPE的函数 :我们将
结果转换为AnyPublisher<UIImage?, Error>
url
fetchImageErr (for url: URL?)

fetchImageErr
Future
Result
Promise
(Result<Output, Failure>) → Void

Future
AnyPublisher <UIImage?, Error>
在“擦除类型”运算符的帮助下eraseToAnyPublisher()
。接下来,我们将执行下列步骤,考虑到所有可能出现的错误(我们不会识别错误,那简直是重要的,我们知道有一个错误):0检查url
用于nil
和 noData
上true
如果是这样,则返回错误,如果不是,传输:url
通过进一步链,1.创建一个dataTaskPublisher(for:)
输入为- 的“发布者” url
,并且输出值为Output
一个元组(data: Data, response: URLResponse)
和一个错误 URLError
,2.使用 tryMap { }
结果元组进行分析(data: Data, response: URLResponse)
:如果它response.statusCode
在范围内200...299
,则为了进行进一步处理,我们仅获取数据 data
。否则,我们会“抛出”错误(无论如何),3.我们map { }
转换数据data
为UIImage
,4传递结果到main
流,因为我们认为我们将在设计中用到它的过程UI
-我们“订阅”接收到的“发行人”用sink
自己的封闭receiveCompletion
和receiveValue
,- 5。如果我们receiveCompletion
发现错误在封闭 error
,我们报告使用它promise (.failure(error)))
,-在封闭6, receiveValue
我们告知成功接收的使用物品的阵列 promise (.success($0))
,在可变7.我们记得收到“订阅” cancellableSet
,以保证类实例的“生命周期”内的生存能力ImageLoader
,8大家“擦除”的“出版商” TYPE并返回实例AnyPublisher
。我们回到ArticleImage
使用新 @Published
变量的地方noData
。如果没有图像数据,那么我们将不显示任何内容,即EmptyView ()
:
最后,我们将把显示NewsAPI.org新闻聚合器中数据的所有可能性打包在TabView
:
访问NewsAPI.org服务器时, 可能会发生错误,例如,由于您指定了错误的密钥,API-key
或者由于开发人员的费用不计任何费用,超出了允许的请求数量或其他原因。同时,NewsAPI.org服务器 为您提供HTTP
代码和相应的消息:
必须处理这种服务器错误。否则,您的应用程序用户将陷入突然的情况,无缘无故,NewsAPI.org服务器 将 停止处理任何请求,使用户完全茫然,出现空白屏幕。到目前为止,从NewsAPI.org服务器 选择文章 [Article]
和信息来源时 [Source]
我们忽略了所有错误,并且在错误出现的情况下返回空数组[Article]()
, 结果是 [Source]()
。从错误处理开始,让我们基于现有的fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
文章选择NewsAPI
方法在类中创建另一个方法,该 方法fetchArticlesErr (from endpoint: Endpoint) -> AnyPublisher<[Article], NewsError>
不仅将返回文章数组[Article]
,而且还可能返回错误 NewsError
:func fetchArticlesErr(from endpoint: Endpoint) ->
AnyPublisher<[Article], NewsError> {
. . . . . . . .
}
此方法与方法一样fetchArticles
,在输入处接受endpoint
并返回带有文章数组形式的值的“发布者” [Article]
,但不是没有错误Never
,而是由枚举定义了一个错误NewsError
:
我们通过初始化“发布者”开始创建新方法 Future
,该方法用于Result
使用闭包异步获取单个TYPE值。闭包有一个参数- Promise
这是TYPE的函数 (Result<Output, Failure>) -> Void
:我们将使用“ TYPE Erase”运算符将
接收Future
到的信息转换成我们需要的“ publisher” 。
在新方法中,我们将重复执行该方法中的所有步骤 AnyPublisher <[Article], NewsError>
eraseToAnyPublisher()
fetchArticlesErr
fetchArticles
,但我们会考虑所有可能的错误:
- 0. endpoint
URL endpoint.absoluteURL
, url
nil
: nil
, .urlError
, — url
, - 1. «»
dataTaskPublisher(for:)
, — url
, Output
(data: Data, response: URLResponse)
URLError
, - 2.
tryMap { }
(data: Data, response: URLResponse)
: response.statusCode
200...299
, data
. «» .responseError
, data
, String
, , - 3.
JSON
, NewsResponse
, - 4.
main
, UI
- «» «»
sink
receiveCompletion
receiveValue
,
- 5.
receiveCompletion
error
, promise (.failure(...)))
, - 6.
receiveValue
promise (.success($0.articles))
,
- 7. «»
var subscriptions
, « » NewsAPI
, - 8. «» «»
AnyPublisher
.
应该注意的是,“发布者” dataTaskPublisher(for:)
与它的原型的不同之处dataTask
在于,如果不在response.statusCode
范围内时发生服务器错误200...299
,它仍然以元组的形式传递成功的值(data: Data, response: URLResponse)
,而不是形式上的错误(Error, URLResponse?)
。在这种情况下,真实的服务器错误信息包含在中data
。如果客户端发生错误(无法与服务器联系,安全系统禁令等),则“发布者” dataTaskPublisher(for:)
将发送错误 。
如果要在中显示错误,则需要相应的错误,我们将其称为 :
在实现协议的类中,这次我们有四个 属性:URLError
ATS
SwiftUI
View Model
ArticlesViewModelErr

ArticlesViewModelErr
ObservableObject
@Published
@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , Endpoint
: «» , ( «», View
), -
@Published var articles: [Article]
- ( «», NewsAPI.org ) -
@Published var articlesError: NewsError?
- , NewsAPI.org .
当您初始化一个类的实例时ArticlesViewModelErr
,我们必须再次从输入“ publishers” 到输出“ publisher” 扩展一条链,$indexEndpoint
并用“ Subscriber”对其进行“签名”,从而得到很多文章或错误 。
在我们的课堂上,我们已经构造了 一个根据值返回``发布者'' 的函数,我们只需要以某种方式使用``发布者''的值 并将 它们变成该函数的参数即可。 首先,我们将结合“发布者” 和 。为此,有一个运算符:$searchString
AnyPublisher<[Article],NewsError>
sink
articles
articlesError
NewsAPI
fetchArticlesErr (from endpoint: Endpoint)
AnyPublisher<[Article], NewsError>
endpoint
$indexEndpoint
$searchString
endpoint
$indexEndpoint
$searchString
Combine
Publishers.CombineLatest
然后,我们必须将错误类型TYPE“ publisher”设置为所需的值 NewsError
:
接下来,我们要使用fetchArticlesErr (from endpoint: Endpoint)
类中的函数 NewsAPI
。像往常一样,我们将在操作员的帮助下执行此操作flatMap
,该操作员 将根据从先前“发布者”接收到的数据创建一个新的“发布者”:
然后,在“订阅者”的帮助下“订阅”该新收到的“发布者” sink
并使用其闭包receiveCompletion
和receiveValue
以便从“发布者”接收文章数组的值 articles
或错误articlesError
:
自然地,有必要记住某些外部init()
变量中的结果“订阅” cancellableSet
。否则,我们将无法异步获取该值articles
或错误articlesError
完成后init()
:
为了键入搜索字符串时降低呼叫服务器的数量searchString
,我们不应该使用搜索栏本身的“发行人” $searchString
,但它的修改版本validString
:
“订阅”异步“发行人”,我们创造了init( )
将在类实例的整个“生命周期”中都保持不变 ArticlesViewModelErr
:
我们进行更正 UI
以显示其上可能的数据采样错误。在SwiftU
I中,在现有结构中,我们 ContentVieArticles
使用另一个刚获得的结构 View Model
,只是在名称中添加字母“ Err”。这是该类的一个实例。 ArticlesViewModelErr
,它“捕获”了从NewsAPI.org服务器选择和/或解码文章数据的 错误:
并且,如果出现错误Alert
,我们还添加了紧急消息的显示 。例如,如果错误的API密钥是:struct APIConstants {
static let apiKey: String = "API_KEY"
. . . . . . . . . . . . .
}
......然后我们会收到以下消息:
如果请求的限制已经用完,我们会收到以下消息:
回到选择文章的方法 [Article]
有可能出现的错误 NewsError
,我们可以简化它的代码,如果我们使用 Generic
“出版商” AnyPublisher<T,NewsError>,
该 url
接收JSON
信息异步和直接地在Codable
模型中T
并报告错误 NewsError
:
众所周知,如果该代码的来源数据url
是NewsAPI.orgEndpoint
新闻 汇总者 或国家/地区 ,则很容易使用此代码来获取特定的“发布者”country
信息源,并且输出需要各种模型-例如,文章列表或信息源:

结论
我们了解到HTTP
借助 Combine
其URLSession
“发布者” dataTaskPublisher
和来满足请求是多么容易Codable
。如果您不需要跟踪错误,则可 Generic
以为“发布者” 获得一个非常简单的5行代码,该代码AnyPublisher<T, Never>
可以异步接收JSON
信息并将其 基于给定的 信息直接放置在Codable
模型中:
如果源数据是,此代码非常容易获得特定的发布者,例如,并且输出需要各种模型-例如,一组文章或信息源列表。
如果您需要考虑错误,那么“发布者” 的代码 将稍微复杂一些,但是仍然是非常简单的代码,没有任何回调:T
url

url
Endpoint
Generic
通过使用使用的HTTP
查询执行技术Combine
,您可以创建一个“发布者” AnyPublisher<UIImage?, Never>
来异步选择数据并接收UIImage图像吗?基于URL
。图像ImageLoade
下载器r缓存在内存中,以避免重复的异步数据检索。可以在ObservableObject类中非常容易地使获得的各种“发布者”工作,这些类使用它们的@Published属性来控制使用SwiftUI设计的UI。这些类通常具有视图模型的作用,因为它们具有与活动UI元素(TextField,Stepper,Picker文本框,Toggle单选按钮等)相对应的所谓“ input” @Published属性和输出@Published属性,它主要由被动UI元素(文本文本,图像图像,Circle()几何形状,Rectangle()等)组成。这种想法贯穿了本文介绍的整个NewsAPI.org新闻聚合器应用程序。相当普遍,在为TMDb 电影数据库和Hacker News新闻聚合器 开发应用程序,将在以后的文章中进行讨论。本文的应用程序代码在Github上。PS1.我想引起您的注意的事实是,如果您对本文中介绍的应用程序使用模拟器,则应该知道NavigationLink
模拟器会出现错误。您可以使用NavigationLink
在模拟器上只有1次。那些。您使用了链接,然后返回,单击相同的链接-没有任何反应。在使用另一个链接之前,第一个链接将不起作用,但是第二个链接将变得不可访问。但这只能在模拟器上观察到,在真实设备上一切正常。2.一些信息源仍然http
代替https
其文章的“图片”。如果您确实想查看这些“图片”,但无法控制其外观的来源,则必须配置安全系统ATS ( App Transport Security)
以接收这些http
“图片”,但这当然不是一个好主意。您可以使用更安全的选项。参考文献:
HTTP Swift 5 Combine SwiftUI. 1 .Modern Networking in Swift 5 with URLSession, Combine and Codable.URLSession.DataTaskPublisher’s failure typeCombine: Asynchronous Programming with Swift«SwiftUI & Combine: »Introducing Combine — WWDC 2019 — Videos — Apple Developer. session 722( 722 « Combine» )Combine in Practice — WWDC 2019 — Videos — Apple Developer. session 721( 721 « Combine» )