Modern code for making HTTP requests in Swift 5 using Combine and using them in SwiftUI. Part 1



Querying HTTPis one of the most important skills you need to get when developing iOSapplications. 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 5the new framework of the functional reactive programming Combinein conjunction with the existing URLSession, and Codableprovides 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, Combinewe will create “publishers”Publisherto select data from the Internet, which we can easily “subscribe to” in the future and use when designing UIboth with  UIKitand with help  SwiftUI.

As  SwiftUIit looks more concise and more effectively, because the action of "publishers"  Publisheris not limited to just sample data, and extends further up to the user interface control ( UI). The fact is that SwiftUIdata separation is  View carried out using ObservableObjectclasses with @Publishedproperties, the changes of which are  SwiftUIAUTOMATICALLY monitored and completely redrawn View.

In these  ObservableObjectclasses, 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, Toggleetc.

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 Endpointthat 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  Endpointin 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 ObservableObjectclasses 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-keyor 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 SwiftUIwill be used to a minimum extent without any frills and solely in order to show how Combinewith its "publishers"Publisherand "subscription" are Subscriptionaffected UI.

It is recommended that you register on the NewsAPI.org website and receive the key APIthat 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 Articlewill contain the identifier id, title title, description  description, author author, URL of the “image” urlToImage, date of publication publishedAtand 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 NewsResponseand structure  Articleare Codable, which will allow us to literally two lines of code decoding the JSONdata 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 propertyidwhich we will provide with an artificial unique identifier UUID().

The information source  Sourcewill 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 SourcesResponseand structure  Sourceare Codable, which will allow us to very easily decode the JSONdata 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 Identifiablerequires the presence of the property idthat we already have, so no additional effort will be required from us.

Now consider what we need  APIfor the NewsAPI.org service  and place it in the NewsAPI.swift file  . The central part of ours API is a class NewsAPIthat 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 - Neverand 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 Endpointinitializer 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 endpointand does not return any error - Never:

func fetchArticles(from endpoint: Endpoint) -> AnyPublisher<[Article], Never> {
        guard let url = endpoint.absoluteURL else {              // 0
                    return Just([Article]()).eraseToAnyPublisher()
        }
           return
            URLSession.shared.dataTaskPublisher(for:url)        // 1
            .map{$0.data}                                       // 2
            .decode(type: NewsResponse.self,                    // 3
                    decoder: APIConstants .jsonDecoder)
            .map{$0.articles}                                   // 4
            .replaceError(with: [])                             // 5
            .receive(on: RunLoop.main)                          // 6
            .eraseToAnyPublisher()                              // 7
    }

  • on the basis of the endpoint form URLfor 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 {      // 0
                       return Just([Source]()).eraseToAnyPublisher()
           }
              return
               URLSession.shared.dataTaskPublisher(for:url)      // 1
               .map{$0.data}                                     // 2
               .decode(type: SourcesResponse.self,               // 3
                       decoder: APIConstants .jsonDecoder)
               .map{$0.sources}                                  // 4
               .replaceError(with: [])                           // 5
               .receive(on: RunLoop.main)                        // 6
               .eraseToAnyPublisher()                            // 7
    }

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 Genericfunction fetch(_ url: URL) -> AnyPublisher<T, Error>that returns the  Generic“publisher” AnyPublisher<T, Error>based on URL:

//     URL
     func fetch<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
                   URLSession.shared.dataTaskPublisher(for: url)             // 1
                    .map { $0.data}                                          // 2
                    .decode(type: T.self, decoder: APIConstants.jsonDecoder) // 3
                    .receive(on: RunLoop.main)                               // 4
                    .eraseToAnyPublisher()                                   // 5
    }

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() // 0
         }
         return fetch(url)                                          // 1
             .map { (response: NewsResponse) -> [Article] in        // 2
                             return response.articles }
                .replaceError(with: [Article]())                    // 3
                .eraseToAnyPublisher()                              // 4
     }
    
    //    
    func fetchSources(for country: String)
                                       -> AnyPublisher<[Source], Never> {
        guard let url = Endpoint.sources(country: country).absoluteURL
            else {
                    return Just([Source]()).eraseToAnyPublisher() // 0
        }
        return fetch(url)                                         // 1
            .map { (response: SourcesResponse) -> [Source] in     // 2
                            response.sources }
               .replaceError(with: [Source]())                    // 3
               .eraseToAnyPublisher()                             // 4
    }

The “publishers” thus obtained do not deliver anything until someone “subscribes” to them. We can do this when designing UI.

"Publishers" Publisheras 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 Viewis the “publishers” Publisher. “External changes” can be understood as a timer Timer, notification with NotificationCenter or your Model object, which using the protocol ObservableObjectcan be turned into an external single “source of truth” (source of truth). 

To ordinary "publishers" type Timeror NotificationCenter Viewreacts using the method onReceive (_: perform:). An example of the use of the "publisher" Timerwe 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 SwiftUIexternal "source of truth" (source of truth).

Let's first look at how the SwiftUIreceived "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 Endpointuser chooses, we need to update the list of articles articlesselected from NewsAPI.org . To do this, we will create a very simple class  ArticlesViewModelthat implements a protocol ObservableObject with three  @Publishedproperties:
 


  • @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"  $indexEndpointand  $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, initwe can create a “subscription” that will act throughout the entire “life cycle” of the instance of the class  ArticlesViewModeland reproduce the dependence of the list of articles articleson the index  indexEndpoint and the search string searchString.

To do this, Combinewe extend the chain of "publishers" $indexEndpoint and $searchStringto 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  indexEndpointand searchString, namely from the "publishers" $indexEndpointand $searchStringwho participate in the creation UIwith the help of SwiftUIand we will change them there using the user interface elements  Pickerand TextField.

How will we do this?

We already have a function in our arsenal fetchArticles (from: Endpoint)that is in the class  NewsAPIand returns a "publisher" AnyPublisher<[Article], Never>, depending on the valueEndpoint, and we can only somehow use the values ​​of the "publishers"  $indexEndpointand  $searchStringto turn them into an argument to endpointthis function. 

First, combine the "publishers"  $indexEndpoint and  $searchString. To do this, the Combineoperator 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 cancellableSetthat will save our  AnyCancellable“subscription” in this variable throughout the entire “life cycle” of the class instance  ArticlesViewMode

Therefore, we remove the constant let subscriptionand remember our AnyCancellable“subscription” in the variable  cancellableSetusing 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”  $indexEndpointand / or  searchString, and always thanks to the created “subscription” we will have an array of articles corresponding to the values ​​of these two publishers  articleswithout any additional effort. This  ObservableObjectclass 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 Modelfor our articles, let's start creating the user interface ( UI). In SwiftUIto synchronize Viewwith the ObservableObject Model, a @ObservedObjectvariable is used that refers to an instance of the class of this Model. It is this pair - the  ObservableObject class and the  @ObservedObjectvariable 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 articleViewModeland replace it Text ("Hello, World!")with a list of articles ArticlesListin which we place the articles  articlesViewModel.articlesobtained 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 UIelement to our screen  to control which set of articles we want to display. We will use the  Pickerindex 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 SearchViewfor 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, healthbusiness, 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  ObservableObjectModel that has only two user-controlled @Publishedproperties - searchString and  country:



And again, we use the same scheme: when initializing an instance of a class of a SourcesViewModel class in initwe create a “subscription” that will operate throughout the entire “life cycle” of the class instance  SourcesViewModeland ensure that the list of information sources depends  sourceson the country  countryand the search string  searchString.

With the help  Combinewe pull the chain from the "publishers" $searchString and $countryto 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  cancellableSetusing the operator .store ( in: &self.cancellableSet).

Now that we have View Modelfor our sources of information, let's start creating UI. B SwiftUIto sync ViewcObservableObject The model uses a @ObservedObjectvariable that refers to an instance of the class of this Model.

Add the ContentViewSources class instance to the structure  SourcesViewModelin the form of a variable var sourcesViewModel, remove  Text ("Hello, World!") and place your own Viewfor each of the 3  @Publishedproperties  sourcesViewModel :

 
  • text box  SearchViewfor 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 SearchViewand the “country” with  Picker, and the rest happens AUTOMATICALLY.

The list of sources of information SourcesListcontains 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 NavigationLinkin which destinationwe indicate  DetailSourceViewwhich source data is the source of information  sourceand 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  ArticlesViewModelfor which we must set both “input”  @Publishedproperties:

  • 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 UIImagefor article Article.


The model of the article  Article contains an URLimage urlToImagethat accompanies it  :



Based on this,  URLin 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:

  1. if urlequal nil, return Just(nil),
  2. based on the urlform of the "publisher" dataTaskPublisher(for:), whose output value Outputis a tuple (data: Data, response: URLResponse)and an error  FailureURLError,
  3. we take only data map {}from the tuple (data: Data, response: URLResponse)for further processing  data, and form UIImage,
  4. if the previous steps return error occurs nil,
  5. we deliver the result to the mainstream, as we assume further use in the design UI,
  6. “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 ImageLoaderthat implements the protocol ObservableObject, with two  @Publishedproperties:

  • @Published url: URL? are URLimages
  • @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  flatMapand 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 AnyCancellableusing 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 URLprovided 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 imagewill remain equal nil.

In SwiftUIwe show the image with the help ArticleImagethat 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 Rectanglewith 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  nilyou 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 URLimage, but access to it is closed, and we get a rectangle Rectanglewith rotating text Text("Loading...")that will never be replaced:



In this situation, if the  URLimage is different from  nil, then the equality of the  nilimage  imagemay 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 @Publishedproperties 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 initcatch all the errors Errorthat occur when loading the image and accumulate their presence in the  @Publishedproperty 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 fetchImageErrby initializing the “publisher”  Future, which can be used to asynchronously obtain a single TYPE value Resultusing a closure. The closure has one parameter - Promisewhich is a function of TYPE  (Result<Output, Failure>) → Void: We will turn the



resulting FutureintoAnyPublisher <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 urlfor nil and  noDataon true: if so, then return the error, if not, transfer urlfurther by chain,
1. create a "publisher" dataTaskPublisher(for:)whose input is - url, and the output value Outputis a tuple (data: Data, response: URLResponse)and an error  URLError,
2. analyze using the tryMap { } resulting tuple (data: Data, response: URLResponse): if it response.statusCodeis 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 datato UIImage,
4. deliver the result to the mainstream, since we assume that we will use it later in the design UI
— we “subscribe” to the received “publisher” using sinkits closures receiveCompletionand 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  cancellableSetto 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 ArticleImagewhere we will use the new  @Publishedvariable 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-keyor, 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 HTTPcode 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 Resultusing a closure. The closure has one parameter - Promisewhich is a function of TYPE  (Result<Output, Failure>) -> Void: We will turn the



received Futureinto the "publisher" we need  AnyPublisher <[Article], NewsError>using the "TYPE Erase" operator eraseToAnyPublisher().

Further in the new method, fetchArticlesErrwe 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 dataTaskin 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  URLErrorif 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  ArticlesViewModelErrthat implements the protocol ObservableObject , this time we have FOUR  @Publishedproperties:

  1. @Published var indexEndpoint: Int — Endpoint ( «», View), 
  2. @Published var searchString: String — ,  Endpoint: «» , ( «», View), 
  3.  @Published var articles: [Article] - ( «»,  NewsAPI.org )
  4.   @Published var articlesError: NewsError? - ,    NewsAPI.org .

When you initialize an instance of a class ArticlesViewModelErr, we must again extend a chain from input "publishers" $indexEndpointand $searchStringto output "publisher"  AnyPublisher<[Article],NewsError>, to which we "signed" with the "Subscriber" sinkand we get a lot of articles articlesor error  articlesError.

In our class, NewsAPIwe 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”  $indexEndpointand  $searchStringto turn them into an argument to this function endpoint

To begin with, we will combine the "publishers"  $indexEndpoint and  $searchString. To do this, Combinethere 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  flatMapthat 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” sinkand use its closures receiveCompletionand receiveValueto receive from the “publisher” either the value of an array of articles  articlesor 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 asynchronouslyarticlesor 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 UIin order to display possible data sampling errors on it. In SwiftUI, 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 {
    // News  API key url: https://newsapi.org
    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,  urlreceives JSONinformation asynchronously , places it directly in the CodableModel 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 urlis 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 HTTPrequests with the help of Combineits URLSession“publisher” dataTaskPublisherand Codable. If you don’t need to track errors, you get a very simple Generic5-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 HTTPquery execution using Combine, can you create a “publisher” AnyPublisher<UIImage?, Never>that asynchronously selects data and receives an UIImage image? based on URL. Image ImageLoadedownloaders 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 .

PS

1. 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 NavigationLinkthe simulator works with an error. You can useNavigationLinkon 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 httpinstead httpsfor "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 type
Combine: 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» )

All Articles