Querying HTTP
is one of the most important skills you need to get when developing iOS
applications. In earlier versions Swift
(up to version 5), regardless of whether you generated these requests “from scratch” or using the well-known Alamofire framework , you ended up with complex and confusing callback
type code completionHandler: @escaping(Result<T, APIError>) -> Void
.The appearance in Swift 5
the new framework of the functional reactive programming Combine
in conjunction with the existing URLSession
, and Codable
provides you with all the tools necessary for independent writing very compact code to fetch data from the Internet.In this article, in accordance with the concept, Combine
we will create “publishers”Publisher
to select data from the Internet, which we can easily “subscribe to” in the future and use when designing UI
both with UIKit
and with help SwiftUI
.As SwiftUI
it looks more concise and more effectively, because the action of "publishers" Publisher
is not limited to just sample data, and extends further up to the user interface control ( UI
). The fact is that SwiftUI
data separation is View
carried out using ObservableObject
classes with @Published
properties, the changes of which are SwiftUI
AUTOMATICALLY monitored and completely redrawn View
.In these ObservableObject
classes, you can very simply put a certain business logic of the application, if some of these@Published
properties are the result of synchronous and / or asynchronous transformation other @Published
properties which can be directly changed such "active" elements of the user interface ( UI
) as text boxes TextField
, Picker
, Stepper
, Toggle
etc.To make it clear what is at stake, I will give specific examples. Now many services such as NewsAPI.org and Hacker News offer news aggregators to offer users to choose different sets of articles depending on what interests them. In the case of the NewsAPI.org news aggregator, it can be the latest news, or news in some category - “sport”, “health”, “science”, “technology”, “business”, or news from a specific information source “CNN” , ABC news, Bloomberg, etc. The user usually “expresses” his desires for services in the form Endpoint
that form the necessary one for him URL
.So, using the framework Combine
, you canObservableObject
classes using a very compact code (in most cases no more than 10-12 lines) once to form a synchronous and / or asynchronous dependence of the list of articles on Endpoint
in the form of a "subscription" of "passive" @Published
properties to "active" @Published
properties. This “subscription” will be valid throughout the entire “life cycle” of the ObservableObject
class instance . And then in SwiftUI
you will give the user the opportunity to manage only the “active” @Published
properties in the form Endpoint
, that is, WHAT he wants to see: whether it will be articles with the latest news or articles in the “health” section. The appearance of the articles themselves with the latest news or articles in the "health" section on yours UI
will be provided AUTOMATICALLY by these ObservableObject
classes and their "passive" @Published properties. In codeSwiftUI
you will never need to explicitly request a selection of articles, because ObservableObject
classes that play a role are responsible for their correct and synchronous display on the screen View Model
.I'll show you how this works with NewsAPI.org and Hacker News and the TMDb movie database in a series of articles. In all three cases, approximately the same use pattern will operate Combine
, because in applications of this kind you always have to create LISTS of films or articles, choose the “PICTURES” (images) that accompany them, SEARCH the databases for the needed films or articles using the search bar.When accessing such services, errors may occur, for example, due to the fact that you specified the wrong key API-key
or exceeded the allowable number of requests or something else. You need to handle this kind of error, otherwise you run the risk of leaving the user completely at a loss with a blank screen. Therefore, you need to be able to not only select Combine
data from the Internet using , but also report errors that may occur during sampling, and control their appearance on the screen.We ’ll start developing our strategy by developing an application that interacts with the NewsAPI.org news aggregator . I must say that in this application it SwiftUI
will be used to a minimum extent without any frills and solely in order to show how Combine
with its "publishers"Publisher
and "subscription" are Subscription
affected UI
.It is recommended that you register on the NewsAPI.org website and receive the key API
that is required to complete any requests to the NewsAPI.org service . You must place it in the NewsAPI.swift file in the structure APIConstants
.The application code for this article is on Github .NewsAPI.org Service Data Model and API
The NewsAPI.org service allows you to select information about current news articles [Article]
and their sources [Source]
. Our data model will be very simple, it is located in the Articles.swift file :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?
}
The article Article
will contain the identifier id
, title title
, description description
, author author
, URL of the “image” urlToImage
, date of publication publishedAt
and source of publication source
. Above the articles [Article]
is an add NewsResponse
-in in which we will be interested only in the property articles
, which is an array of articles. The root structure NewsResponse
and structure Article
are Codable
, which will allow us to literally two lines of code decoding the JSON
data into the Model. The structure Article
should also be Identifiable
, if we want to make it easier for ourselves to display an array of articles [Article]
as a list List
in SwiftUI
. Protocol Identifiable
requires the presence of a propertyid
which we will provide with an artificial unique identifier UUID()
.The information source Source
will contain an identifier id
, name name
, description description
, country country
, publication source category category
, site URL url
. Above the information sources [Source]
there is an add SourcesResponse
-in in which we will be interested only in a property sources
, which is an array of information sources. The root structure SourcesResponse
and structure Source
are Codable
, which will allow us to very easily decode the JSON
data into a model. The structure Source
should also be Identifiable
, if we want to facilitate the display of an array of information sources [Source]
in the form of a list List
inSwiftUI
. The protocol Identifiable
requires the presence of the property id
that we already have, so no additional effort will be required from us.Now consider what we need API
for the NewsAPI.org service and place it in the NewsAPI.swift file . The central part of ours API
is a class NewsAPI
that presents two methods for selecting data from the NewsAPI.org news aggregator - articles [Article]
and information sources [Source]
:fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
- selection of articles [Article]
based on the parameter endpoint
,fetchSources (for country: String) -> AnyPublisher<[Source], Never>
- A selection of sources of information [Source]
for a particular country country
.
These methods return not just an array of articles [Article]
or an array of information sources [Source]
, but the corresponding "publishers" of the Publisher
new framework Combine
. Both publishers do not return any error - Never
and if a sampling or coding error still occurred, then an empty array of articles [Article]()
or information sources is returned [Source]()
without any message why these arrays were empty. Which articles or sources of information we want to select from the NewsAPI.org server , we will indicate using the enumerationenum 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"
}
}
}
It:- the latest news
.topHeadLines
, - news of a certain category (sports, healthy, science, business, technology)
.articlesFromCategory(_ category: String)
, - news from a specific source of information (CNN, ABC News, Fox News, etc.)
.articlesFromSource(_ source: String)
, - any news
.search (searchFilter: String)
that meets a certain condition searchFilter
, - sources of information
.sources (country:String)
for a particular country country
.
To facilitate the initialization of the option we need, we will add an Endpoint
initializer init?
to the enumeration for various lists of articles and information sources depending on the index index
and line text
, which has different meanings for different enumeration options: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
}
}
Let's go back to the class NewsAPI
and consider in more detail the first method fetchArticles (from endpoint: Endpoint)-> AnyPublisher<[Article], Never>
, which selects articles [Article]
based on the parameter endpoint
and does not return any error - 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()
}
- on the basis of the
endpoint
form URL
for the request the desired list of articles endpoint.absoluteURL
, if this could not be done, then return an empty array of articles[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
.
The task of selecting information sources is assigned to the second method - fetchSources (for country: String) -> AnyPublisher<[Source], Never>
which is an exact semantic copy of the first method, except that this time instead of articles [Article]
we will choose information sources [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()
}
It returns to us the “publisher” AnyPublisher <[Source], Never>
with a value in the form of an array of information sources [Source]
and no error Never
(in case of errors, an empty array of sources is returned [ ]
).We will single out the common part of these two methods, arrange it as a Generic
function fetch(_ url: URL) -> AnyPublisher<T, Error>
that returns the Generic
“publisher” AnyPublisher<T, Error>
based on 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()
}
This will simplify the previous two methods:
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()
}
The “publishers” thus obtained do not deliver anything until someone “subscribes” to them. We can do this when designing UI
."Publishers" Publisher
as View Model
in SwiftUI
. List of articles.
Now a little about the logic of functioning SwiftUI
.The SwiftUI
only abstraction of the “external changes” to which they respond View
is the “publishers” Publisher
. “External changes” can be understood as a timer Timer
, notification with NotificationCenter
or your Model object, which using the protocol ObservableObject
can be turned into an external single “source of truth” (source of truth). To ordinary "publishers" type Timer
or NotificationCenter
View
reacts using the method onReceive (_: perform:)
. An example of the use of the "publisher" Timer
we will present later in the third article on the creation of an application for Hacker News .In this article, we will focus on how to make our Model for SwiftUI
external "source of truth" (source of truth).Let's first look at how the SwiftUI
received "publishers" should function in a specific example of displaying various kinds of articles:.topHeadLines
- the latest news, .articlesFromCategory(_ category: String)
- news for a specific category, .articlesFromSource(_ source: String)
- news for a specific source of information, .search (searchFilter: String)
- news selected by a certain condition.
Depending on which Endpoint
user chooses, we need to update the list of articles articles
selected from NewsAPI.org . To do this, we will create a very simple class ArticlesViewModel
that implements a protocol ObservableObject
with three @Published
properties: 
@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , ( «», View
TextField
),@Published var articles: [Article]
- ( «», NewsAPI.org, «»).
As soon as we set @Published
properties indexEndpoint
or searchString
, we can begin to use them both as simple properties indexEndpoint
and searchString
, and as "publishers" $indexEndpoint
and $searchString
.In a class ArticlesViewModel
, you can not only declare the properties of interest to us, but also prescribe the business logic of their interaction. For this purpose, when initializing an instance of a class ArticlesViewModel
in, init
we can create a “subscription” that will act throughout the entire “life cycle” of the instance of the class ArticlesViewModel
and reproduce the dependence of the list of articles articles
on the index indexEndpoint
and the search string searchString
.To do this, Combine
we extend the chain of "publishers" $indexEndpoint
and $searchString
to output "publisher"AnyPublisher<[Article], Never>
whose value is a list of articles articles
. Then we “subscribe” to it using the operator assign (to: \.articles, on: self)
and get the list of articles we need articles
as an “output” @Published
property that defines UI
.We must pull the chain NOT simply from the properties indexEndpoint
and searchString
, namely from the "publishers" $indexEndpoint
and $searchString
who participate in the creation UI
with the help of SwiftUI
and we will change them there using the user interface elements Picker
and TextField
.How will we do this?We already have a function in our arsenal fetchArticles (from: Endpoint)
that is in the class NewsAPI
and returns a "publisher" AnyPublisher<[Article], Never>
, depending on the valueEndpoint
, and we can only somehow use the values ​​of the "publishers" $indexEndpoint
and $searchString
to turn them into an argument to endpoint
this function. First, combine the "publishers" $indexEndpoint
and $searchString
. To do this, the Combine
operator exists Publishers.CombineLatest
:
To create a new “publisher” based on the data received from the previous “publisher” Combine
, the operator is used flatMap
:
Next, we “subscribe” to this newly received “publisher” using a very simple “subscriber” assign (to: \.articles, on: self)
and assign the received from “ publisher "value to the @Published
array articles
:
We just created an init( )
ASYNCHRONOUS" publisher "and" subscribed "to it, as a result ofAnyCancellable
“Subscription” and this is easy to verify if we keep our “subscription” in a constant let subscription
:
The main property of a AnyCancellable
“subscription” is that as soon as it leaves its scope, the memory occupied by it is automatically freed. Therefore, as soon as it is init( )
completed, this “subscription” will be deleted ARC
, without having time to assign the asynchronous information received with a time delay to the array articles
. Asynchronous information simply has nowhere to "land", in its literal sense, "the earth has gone from under its feet."To save such a “subscription”, it is necessary to create a init()
variable BEYOND the initializer var cancellableSet
that will save our AnyCancellable
“subscription” in this variable throughout the entire “life cycle” of the class instance ArticlesViewMode
. Therefore, we remove the constant let subscription
and remember our AnyCancellable
“subscription” in the variable cancellableSet
using the operator .store ( in: &self.cancellableSet)
:
“Subscription” to the ASYNCHRONOUS “publisher” that we created in init( )
will be preserved throughout the entire “life cycle” of the class instance ArticlesViewModel
.We can arbitrarily change the meaning of “publishers” $indexEndpoint
and / or searchString
, and always thanks to the created “subscription” we will have an array of articles corresponding to the values ​​of these two publishers articles
without any additional effort. This ObservableObject
class is usually called View Model
.In order to reduce the number of calls to the server when typing a search string searchString
, we should not use the “publisher” of the search string itself $searchString
, and its modified version validString
:
Now that we have View Model
for our articles, let's start creating the user interface ( UI
). In SwiftUI
to synchronize View
with the ObservableObject
Model, a @ObservedObject
variable is used that refers to an instance of the class of this Model. It is this pair - the ObservableObject
class and the @ObservedObject
variable that references the instance of this class - that control the change in the user interface ( UI
) in SwiftUI
.We add to the structure an ContentView
instance of the class ArticleViewModel
in the form of a variable var articleViewModel
and replace it Text ("Hello, World!")
with a list of articles ArticlesList
in which we place the articles articlesViewModel.articles
obtained from our View Model
. As a result, we get a list of articles for a fixed and default index indexEndpoint = 0
, that is, for the .topHeadLines
latest news:
Add an UI
element to our screen to control which set of articles we want to display. We will use the Picker
index change $articlesViewModel.indexEndpoint
. The presence of a symbol is $
mandatory, as this means a change in the value supplied by the @Published
"publisher". The “subscription” to this “publisher” is triggered immediately, which we initiated on init ()
, the “output” @Published
“publisher” articles
will change and we will see a different list of articles on the screen:
In this way we can receive arrays of articles for all three options - “topHeadLines”, “search "And" from category ":
... but for a fixed and default search string searchString = "sports"
(where it is required):
However, for the option, "search"
you must provide the user with a text field SearchView
for entering the search string:
As a result, the user can search for any news by the typed search string:
For the option, "from category"
it is necessary to provide the user the opportunity to choose a category and we start with category science
:
as a result, the user can search for any news on the chosen category of news - science
, health
, business
, technology
:
we can see how a very simple ObservableObject
model that has two user-controlled @Published
features - indexEndpoint
andsearchString
- allows you to select a wide range of information from the NewsAPI.org website .List of sources of information
Let's look at how the SwiftUI
“publisher” of information sources received in the NewsAPI class will function fetchSources (for country: String) -> AnyPublisher<[Source], Never>
.We will get a list of sources of information for different countries:
... and the ability to search for them by name:
... as well as detailed information about the selected source: its name, category, country, short description and link to the site:
If you click on the link, we will go to the website of this source of information.For all this to work, you need an extremely simple ObservableObject
Model that has only two user-controlled @Published
properties - searchString
and country
:
And again, we use the same scheme: when initializing an instance of a class of a SourcesViewModel
class in init
we create a “subscription” that will operate throughout the entire “life cycle” of the class instance SourcesViewModel
and ensure that the list of information sources depends sources
on the country country
and the search string searchString
.With the help Combine
we pull the chain from the "publishers" $searchString
and $country
to output "publisher" AnyPublisher<[Source], Never>
, whose value is a list of information sources. We “subscribe” to it using the operator assign (to: \.sources, on: self)
, we get the list of information sources we need sources
. and remember the received AnyCancellable
"subscription" in a variable cancellableSet
using the operator .store ( in: &self.cancellableSet)
.Now that we have View Model
for our sources of information, let's start creating UI
. B SwiftUI
to sync View
cObservableObject
The model uses a @ObservedObject
variable that refers to an instance of the class of this Model.Add the ContentViewSources
class instance to the structure SourcesViewModel
in the form of a variable var sourcesViewModel
, remove Text ("Hello, World!")
and place your own View
for each of the 3 @Published
properties sourcesViewModel
: - text box
SearchView
for the search bar searchString
, -
Picker
for the country country
, - list of
SourcesList
sources of information
As a result, we get what we need View
:
On this screen, we only manage the search string using the text field SearchView
and the “country” with Picker
, and the rest happens AUTOMATICALLY.The list of sources of information SourcesList
contains minimal information about each source - the name source.name
and a brief description source.description
:
... but allows you to get more detailed information about the selected source using the link NavigationLink
in which destination
we indicate DetailSourceView
which source data is the source of information source
and the desired instance of the class ArticlesViewModel
, allowing get a list of his articles articles
:
See how elegantly we get the list of articles for the selected source of information source in the list of sources SourcesList
. Our old friend helps us - a class ArticlesViewModel
for which we must set both “input” @Published
properties:- index
indexEndpoint = 3
, that is, an option .articlesFromSource (_source:String)
corresponding to the selection of articles for a fixed source source
, - string
searchString
as the source itself (or rather its identifier) source.id
:
In general, if you look at the entire NewsApp application , you will not see anywhere that we explicitly request a selection of articles or information sources from the NewsAPI.org website . We only manage @Published
data, but View Model
does our job: selects the articles and sources of information we need.Download image UIImage
for article Article
.
The model of the article Article
contains an URL
image urlToImage
that accompanies it :
Based on this, URL
in the future we must obtain the images themselves UIImage
from the NewsAPI.org website .We are already familiar with this task. In the class ImageLoader
, using the function, fetchImage(for url: URL?) -> AnyPublisher<UIImage?, Never>
create a “publisher” AnyPublisher<UIImage?, Never>
with the image value UIImage?
and no error Never
(in fact, if errors occur, then the image is returned nil
). You can “subscribe” to this “publisher” to receive images UIImage?
when designing the user interface ( UI
). The source data for the function fetchImage(for url: URL?)
is url
, which we have:
Let us consider in detail how the formation is being carried out using the Combine
“publisher” AnyPublisher <UIImage?, Never>
, if we know url
:- if
url
equal nil
, return Just(nil)
, - based on the
url
form of the "publisher" dataTaskPublisher(for:)
, whose output value Output
is a tuple (data: Data, response: URLResponse)
and an error Failure
- URLError
, - we take only data
map {}
from the tuple (data: Data, response: URLResponse)
for further processing data
, and form UIImage
, - if the previous steps return error occurs
nil
, - we deliver the result to the
main
stream, as we assume further use in the design UI
, - “Erase” the TYPE of the “publisher” and return the copy
AnyPublisher
.
You see that the code is quite compact and well readable, there aren't any callbacks
.Let's start creating View Model
for the image UIImage?
. This is a class ImageLoader
that implements the protocol ObservableObject
, with two @Published
properties:@Published url: URL?
are URL
images@Published var image: UIImage?
is the image itself from NewsAPI.org :
And again, when initializing an instance of the class, ImageLoader
we must stretch the chain from the input "publisher" $url
to the output "publisher" AnyPublisher<UIImage?, Never>
, which we will "subscribe" to later and get the image we need image
:
We use the operator flatMap
and a very simple "subscriber" assign (to: \image, on: self)
to assign it to the received from the "publisher "Values ​​to the property @Published image
:
And again in the variable " subscription "is cancellableSet
stored AnyCancellable
using the operator store(in: &self.cancellableSet)
.The logic of this “image downloader” is that you download an image from something other than the one nil URL
provided that it did not preload, that isimage == nil
. If during the download process any error is detected, the image will be absent, that is, it image
will remain equal nil
.In SwiftUI
we show the image with the help ArticleImage
that an instance of the imageLoader
class uses for this ImageLoader
. If his image image is not equal nil
, then it is displayed using Image (...)
, but if it is equal nil
, then depending on what it is equal to url
, either nothing is shown EmptyView()
, or a rectangle Rectangle
with rotating text T is displayed ext("Loading...")
:
This logic works just fine for the case when you know for sure that for url
, other than nil
you get an image image
, as is the case with the movie database TMDb . With NewsAPI.org, the news aggregator is different. The articles of some sources of information give a different one from the nil URL
image, but access to it is closed, and we get a rectangle Rectangle
with rotating text Text("Loading...")
that will never be replaced:
In this situation, if the URL
image is different from nil
, then the equality of the nil
image image
may mean that the image is loading , and the fact that an error occurred while loading and we will never get an image image
. In order to distinguish between these two situations, we add one more ImageLoader
to the two existing @Published
properties in the class : @Published var noData = false
- this is a Boolean value with which we will denote the absence of image data due to an error during the selection:
When creating a “subscription”, we init
catch all the errors Error
that occur when loading the image and accumulate their presence in the @Published
property self.noData = true
. If the download was successful, then we get the image image
. We create the“Publisher” AnyPublisher<UIImage?, Error>
on the basis of the url
function fetchImageErr (for url: URL?)
:
We begin to create a method fetchImageErr
by initializing the “publisher” Future
, which can be used to asynchronously obtain a single TYPE value Result
using a closure. The closure has one parameter - Promise
which is a function of TYPE (Result<Output, Failure>) → Void
: We will turn the
resulting Future
intoAnyPublisher <UIImage?, Error>
with the help of the “Erase TYPE” operator eraseToAnyPublisher()
.Next, we will perform the following steps, taking into account all possible errors (we will not identify errors, it is simply important for us to know that there is an error):0. check url
for nil
and noData
on true
: if so, then return the error, if not, transfer url
further by chain,1. create a "publisher" dataTaskPublisher(for:)
whose input is - url
, and the output value Output
is a tuple (data: Data, response: URLResponse)
and an error URLError
,2. analyze using the tryMap { }
resulting tuple (data: Data, response: URLResponse)
: if it response.statusCode
is in the range 200...299
, then for further processing we take only the data data
. Otherwise, we "throw out" an error (no matter what),3. we will map { }
convert the data data
to UIImage
,4. deliver the result to the main
stream, since we assume that we will use it later in the design UI
— we “subscribe” to the received “publisher” using sink
its closures receiveCompletion
and receiveValue
,- 5. if we receiveCompletion
find an error in the closure error
, we report using it promise (.failure(error)))
,- 6. in the closure, receiveValue
we inform about the successful receipt of an array of articles using promise (.success($0))
,7. we remember the received “subscription” in the variable cancellableSet
to ensure its viability within the “lifetime” of the class instance ImageLoader
,8. we “erase” the “publisher” TYPE and return the instance AnyPublisher
.We return to ArticleImage
where we will use the new @Published
variable noData
. If there is no image data, then we will not display anything, that is EmptyView ()
:
Finally, we will pack all our possibilities of displaying data from the NewsAPI.org news aggregator in TabView
:
Display errors when fetching and decoding JSON data from the NewsAPI.org server .
When accessing the NewsAPI.org server, errors can occur, for example, due to the fact that you specified the wrong key API-key
or, having a developer’s tariff that costs nothing, exceeded the allowed number of requests or something else. At the same time, the NewsAPI.org server provides you with the HTTP
code and the corresponding message:
It is necessary to handle this kind of server error. Otherwise, the user of your application will fall into a situation when suddenly, for no reason, the NewsAPI.org server will stop processing any requests, leaving the user completely at a loss with a blank screen.Until now, when selecting articles [Article]
and sources of information [Source]
from the NewsAPI.org server we ignored all errors, and in case of their appearance returned empty arrays [Article]()
and as a result [Source]()
.Getting started with error handling, let's fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
create NewsAPI
another method in the class based on the existing article selection method fetchArticlesErr (from endpoint: Endpoint) -> AnyPublisher<[Article], NewsError>
that will return not only an array of articles [Article]
, but also a possible error NewsError
:func fetchArticlesErr(from endpoint: Endpoint) ->
AnyPublisher<[Article], NewsError> {
. . . . . . . .
}
This method, as well as the method fetchArticles
, accepts endpoint
and returns the “publisher” at the input with a value in the form of an array of articles [Article]
, but instead of the absence of an error Never
, we may have an error defined by the enumeration NewsError
:
We start creating a new method by initializing the “publisher” Future
, which use to asynchronously obtain a single TYPE value Result
using a closure. The closure has one parameter - Promise
which is a function of TYPE (Result<Output, Failure>) -> Void
: We will turn the
received Future
into the "publisher" we need AnyPublisher <[Article], NewsError>
using the "TYPE Erase" operator eraseToAnyPublisher()
.Further in the new method, fetchArticlesErr
we will repeat all the steps that we took in the method fetchArticles
, but we will take into account all possible errors:
- 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
.
It should be noted that the "publisher" dataTaskPublisher(for:)
differs from its prototype dataTask
in that in case of a server error when it is response.statusCode
NOT in the range 200...299
, it still delivers the successful value in the form of a tuple (data: Data, response: URLResponse)
, and not an error in the form (Error, URLResponse?)
. In this case, the real server error information is contained in data
. The "publisher" dataTaskPublisher(for:)
delivers an error URLError
if an error occurs on the client side (inability to contact the server, security system ban ATS
, etc.).If we want to display errors in SwiftUI
, then we need the corresponding one View Model
, which we will call ArticlesViewModelErr
:
In the class ArticlesViewModelErr
that implements the protocol ObservableObject
, this time we have FOUR @Published
properties:@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , Endpoint
: «» , ( «», View
), -
@Published var articles: [Article]
- ( «», NewsAPI.org ) -
@Published var articlesError: NewsError?
- , NewsAPI.org .
When you initialize an instance of a class ArticlesViewModelErr
, we must again extend a chain from input "publishers" $indexEndpoint
and $searchString
to output "publisher" AnyPublisher<[Article],NewsError>
, to which we "signed" with the "Subscriber" sink
and we get a lot of articles articles
or error articlesError
.In our class, NewsAPI
we have already constructed a function fetchArticlesErr (from endpoint: Endpoint)
that returns a “publisher” AnyPublisher<[Article], NewsError>
, depending on the value endpoint
, and we only need to somehow use the values ​​of the “publishers” $indexEndpoint
and $searchString
to turn them into an argument to this function endpoint
. To begin with, we will combine the "publishers" $indexEndpoint
and $searchString
. To do this, Combine
there is an operator Publishers.CombineLatest
:
Then we must set the error type TYPE "publisher" equal to the required NewsError
:
Next, we want to use the function fetchArticlesErr (from endpoint: Endpoint)
from our class NewsAPI
. As usual, we will do this with the help of an operator flatMap
that creates a new “publisher” based on data received from the previous “publisher”:
Then we “subscribe” to this newly received “publisher” with the help of a “subscriber” sink
and use its closures receiveCompletion
and receiveValue
to receive from the “publisher” either the value of an array of articles articles
or errors articlesError
:
Naturally, it is necessary to remember the resulting “subscription” in some external init()
variable cancellableSet
. Otherwise, we will not be able to get the value asynchronouslyarticles
or an error articlesError
after completion init()
:
In order to reduce the number of calls to the server when typing a search string searchString
, we should not use the “publisher” of the search bar itself $searchString
, but its modified version validString
:
“Subscribing” to the ASYNCHRONOUS “publisher” that we created in init( )
will be persist throughout the entire “life cycle” of the class instance ArticlesViewModelErr
:
We proceed to the correction of ours UI
in order to display possible data sampling errors on it. In SwiftU
I, in the existing structure, we ContentVieArticles
use another, just obtained View Model
, just adding the letters “Err” in the name. This is an instance of the class. ArticlesViewModelErr
, which “catches” the error of selecting and / or decoding article data from the NewsAPI.org server :
And we also add the display of an emergency message Alert
in case of an error.For example, if the wrong API key is:struct APIConstants {
static let apiKey: String = "API_KEY"
. . . . . . . . . . . . .
}
... then we will get this message:
If the limit of requests has been exhausted, then we will get this message:
Returning to the method of selecting articles [Article]
with a possible error NewsError
, we can simplify its code if we use a Generic
"publisher" AnyPublisher<T,NewsError>,
which, based on the set, url
receives JSON
information asynchronously , places it directly in the Codable
Model T
and reports an error NewsError
:
As we know, this code is very easy to use to obtain a specific “publisher” if the source data for url
is a NewsAPI.orgEndpoint
news aggregator or country country
information source, and the output requires various Models - for example, a list of articles or information sources:

Conclusion
We learned how easy it is to fulfill HTTP
requests with the help of Combine
its URLSession
“publisher” dataTaskPublisher
and Codable
. If you don’t need to track errors, you get a very simple Generic
5-line code for the “publisher” AnyPublisher<T, Never>
, which asynchronously receives JSON
information and places it directly in the Codable
Model T
based on the given url
:
This code is very easy to use to get a specific publisher, if the source data url
is, for example Endpoint
, and the output requires various Models - for example, a set of articles or a list of sources of information.If you need to take into account errors, the code for the Generic
"publisher" will be a little more complicated, but still it will be very simple code without any callbacks:
Using the technology of HTTP
query execution using Combine
, can you create a “publisher” AnyPublisher<UIImage?, Never>
that asynchronously selects data and receives an UIImage image? based on URL
. Image ImageLoade
downloaders r are cached in memory to avoid repeated asynchronous data retrieval.All kinds of "publishers" obtained can very easily be "made to work" in ObservableObject classes, which use their @Published properties to control your UI designed using SwiftUI. These classes usually play the role of the View Model, since they have the so-called “input” @Published properties that correspond to the active UI elements (TextField, Stepper, Picker text boxes, Toggle radio buttons, etc.) and “output” @Published properties , consisting mainly of passive UI elements (Text, Image, Image, Circle (), Rectangle (), etc.This idea permeates the entire NewsAPI.org news aggregator application presented in this article. It turned out to be quite universal and was used whendeveloping an application for the TMDb movie database and the Hacker News news aggregator , which will be discussed in future articles.The application code for this article is on Github .PS1. I want to draw your attention to the fact that if you use the simulator for the application presented in this article, then know that NavigationLink
the simulator works with an error. You can useNavigationLink
on the simulator only 1 time. Those. you used the link, went back, click on the same link - and nothing happens. Until you use another link, the first will not work, but the second will become inaccessible. But this is only observed on the simulator, on a real device everything works fine.2. Some sources of information still use http
instead https
for "pictures" of their articles. If you definitely want to see these “pictures”, but cannot control the source of their appearance, then you will have to configure the security system ATS ( App Transport Security)
to receive these http
“pictures”, but this, of course, is not a good idea . You can use more secure options .References:
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» )