L'interrogation HTTP
est l'une des compétences les plus importantes que vous devez acquérir lors du développement d' iOS
applications. Dans les versions antérieures Swift
(avant la version 5), que vous ayez généré ces requêtes à partir de zéro ou que vous utilisiez le cadre Alamofire bien connu , vous vous êtes retrouvé avec du code complexe et déroutant du callback
type completionHandler: @escaping(Result<T, APIError>) -> Void
.L'apparition dans Swift 5
le nouveau cadre de la programmation réactive fonctionnelle Combine
en conjonction avec l'existant URLSession
, et Codable
vous fournit tous les outils nécessaires à l'écriture indépendante de code très compact pour récupérer des données sur Internet.Dans cet article, conformément au concept, Combine
nous allons créer des «éditeurs»Publisher
pour sélectionner des données sur Internet, auxquelles nous pouvons facilement «souscrire» à l'avenir et utiliser lors de la conception UI
avec UIKit
et avec l'aide SwiftUI
.Comme SwiftUI
il semble plus concis et plus efficace, car l'action des "éditeurs" Publisher
ne se limite pas à des exemples de données et s'étend plus loin jusqu'au contrôle de l'interface utilisateur ( UI
). Le fait est que SwiftUI
la séparation des données est View
effectuée à l'aide de ObservableObject
classes avec des @Published
propriétés, dont les modifications SwiftUI
suivent AUTOMATIQUEMENT et «redessinent» complètement les leurs View
.Dans ces ObservableObject
classes, vous pouvez très simplement mettre une certaine logique métier de l'application, si certains d'entre eux@Published
propriétés sont le résultat de la transformation synchrones et / ou asynchrones d' autres @Published
propriétés qui peuvent être modifiés directement ces éléments « actifs » de l'interface utilisateur ( UI
) en tant que zones de texte TextField
, Picker
, Stepper
, Toggle
etc.Pour clarifier les enjeux, je vais donner des exemples précis. Maintenant, de nombreux services tels que NewsAPI.org et Hacker News proposent des agrégateurs de nouvelles pour offrir aux utilisateurs de choisir différents ensembles d'articles en fonction de ce qui les intéresse. Dans le cas de l' agrégateur d' actualités NewsAPI.org, il peut s'agir des dernières nouvelles, ou des nouvelles dans une catégorie - «sport», «santé», «science», «technologie», «entreprise», ou des nouvelles d'une source d'information spécifique «CNN» , ABC news, Bloomberg, etc. L'utilisateur «exprime» généralement ses désirs de services sous la forme Endpoint
qui lui est nécessaire URL
.Donc, en utilisant le cadre Combine
, vous pouvezObservableObject
classes utilisant un code très compact (dans la plupart des cas pas plus de 10-12 lignes) une fois pour former une dépendance synchrone et / ou asynchrone de la liste d'articles Endpoint
sous la forme d'un "abonnement" de @Published
propriétés "passives" à des propriétés "actives" @Published
. Cet «abonnement» sera valide pendant tout le «cycle de vie» de l'instance de ObservableObject
classe. Et ensuite, SwiftUI
vous donnerez à l'utilisateur la possibilité de gérer uniquement les @Published
propriétés «actives» dans le formulaire Endpoint
, c'est-à-dire CE qu'il veut voir: qu'il s'agisse d'articles avec les dernières nouvelles ou d'articles dans la section «santé». L'apparition des articles eux-mêmes avec les dernières nouvelles ou articles dans la section "santé" sur le vôtre UI
sera fournie AUTOMATIQUEMENT par ces ObservableObject
classes et leurs propriétés "passives" @Published. Dans du codeSwiftUI
vous n'aurez jamais besoin de demander explicitement une sélection d'articles, car les ObservableObject
classes qui jouent un rôle sont responsables de leur affichage correct et synchrone à l'écran View Model
.Je vais vous montrer comment cela fonctionne avec NewsAPI.org et Hacker News et la base de données de films TMDb dans une série d'articles. Dans les trois cas, le même schéma d'utilisation fonctionnera approximativement Combine
, car dans les applications de ce type, vous devez toujours créer des LISTES de films ou d'articles, choisissez les «IMAGES» (images) qui les accompagnent, RECHERCHEZ les bases de données des films ou articles nécessaires à l'aide de la barre de recherche.Lors de l'accès à ces services, des erreurs peuvent se produire, par exemple, du fait que vous avez spécifié la mauvaise clé API-key
ou dépassé le nombre autorisé de demandes ou autre chose. Vous devez gérer ce type d'erreur, sinon vous courez le risque de laisser l'utilisateur complètement à perte avec un écran vide. Par conséquent, vous devez être en mesure non seulement de sélectionner des Combine
données sur Internet à l' aide , mais également de signaler les erreurs qui peuvent survenir lors de l'échantillonnage et de contrôler leur apparence à l'écran.Nous allons commencer à développer notre stratégie en développant une application qui interagit avec l' agrégateur d' actualités NewsAPI.org . Je dois dire que dans cette application, il SwiftUI
sera utilisé dans une mesure minimale sans fioritures et uniquement pour montrer comment Combine
avec ses "éditeurs"Publisher
et "abonnement" sont Subscription
affectés UI
.Il est recommandé de vous inscrire sur le site Web NewsAPI.org et de recevoir la clé API
nécessaire pour répondre à toute demande au service NewsAPI.org . Vous devez le placer dans le fichier NewsAPI.swift de la structure APIConstants
.Le code d'application de cet article est sur Github .API et modèle de données de service NewsAPI.org
Le service NewsAPI.org vous permet de sélectionner des informations sur les articles d'actualité [Article]
et leurs sources [Source]
. Notre modèle de données sera très simple, il se trouve dans le fichier 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?
}
L'article Article
contiendra l'identifiant id
, le titre title
, la description description
, l'auteur author
, l'URL de «l'image» urlToImage
, la date de publication publishedAt
et la source de publication source
. Au-dessus des articles se [Article]
trouve un complément NewsResponse
dans lequel nous ne nous intéresserons qu'à la propriété articles
, qui est un tableau d'articles. La structure racine NewsResponse
et la structure Article
sont Codable
, ce qui nous permettra littéralement deux lignes de code décodant les JSON
données dans le modèle. La structure Article
devrait également être la suivante Identifiable
, si nous voulons faciliter l'affichage d'un tableau d'articles [Article]
sous forme de liste List
dans SwiftUI
. Le protocole Identifiable
nécessite la présence d'un bienid
que nous fournirons avec un identifiant unique artificiel UUID()
.La source d'informations Source
contiendra un identifiant id
, un nom name
, une description description
, un pays country
, une catégorie de source de publication category
et une URL de site url
. Au-dessus des sources d'informations, [Source]
il existe un complément SourcesResponse
dans lequel nous ne nous intéresserons qu'à une propriété sources
, qui est un tableau de sources d'informations. La structure racine SourcesResponse
et la structure Source
sont Codable
, ce qui nous permettra de décoder très facilement les JSON
données dans un modèle. La structure Source
devrait également être Identifiable
, si nous voulons faciliter l'affichage d'un tableau de sources d'information [Source]
sous la forme d'une liste List
dansSwiftUI
. Le protocole Identifiable
nécessite la présence de la propriété id
que nous avons déjà, donc aucun effort supplémentaire ne sera requis de notre part.Considérez maintenant ce dont nous avons besoin API
pour le service NewsAPI.org et placez-le dans le fichier NewsAPI.swift . La partie centrale de la nôtre API
est une classe NewsAPI
qui présente deux méthodes pour sélectionner les données de l' agrégateur de news NewsAPI.org - articles [Article]
et sources d'information [Source]
:fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
- sélection d'articles en [Article]
fonction du paramètre endpoint
,fetchSources (for country: String) -> AnyPublisher<[Source], Never>
- Une sélection de sources d'informations [Source]
pour un pays particulier country
.
Ces méthodes renvoient non seulement un tableau d'articles [Article]
ou un tableau de sources d'informations [Source]
, mais les "éditeurs" correspondants du Publisher
nouveau cadre Combine
. Les deux éditeurs ne renvoient aucune erreur - Never
et si une erreur d'échantillonnage ou de codage s'est toujours produite, un tableau vide d'articles [Article]()
ou de sources d'informations est renvoyé [Source]()
sans aucun message expliquant pourquoi ces tableaux étaient vides. Quels articles ou sources d'informations nous voulons sélectionner sur le serveur NewsAPI.org , nous indiquerons en utilisant l'énumérationenum 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"
}
}
}
Il:- les dernières nouvelles
.topHeadLines
, - l'actualité d'une certaine catégorie (sport, santé, science, business, technologie)
.articlesFromCategory(_ category: String)
, - l'actualité d'une source d'information spécifique (CNN, ABC News, Fox News, etc.)
.articlesFromSource(_ source: String)
, - toute nouvelle
.search (searchFilter: String)
qui remplit une certaine condition searchFilter
, - sources d'information
.sources (country:String)
pour un pays particulier country
.
Pour faciliter l'initialisation de l'option dont nous avons besoin, nous ajouterons un Endpoint
initialiseur init?
à l' énumération pour diverses listes d'articles et de sources d'informations en fonction de l'index index
et de la ligne text
, ce qui a différentes significations pour différentes options d'énumération: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
}
}
Revenons à la classe NewsAPI
et considérons plus en détail la première méthode fetchArticles (from endpoint: Endpoint)-> AnyPublisher<[Article], Never>
, qui sélectionne les articles en [Article]
fonction du paramètre endpoint
et ne renvoie aucune erreur - 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()
}
- sur la base du
endpoint
formulaire URL
de demande la liste d'articles souhaitée endpoint.absoluteURL
, en cas d'échec, puis renvoyer un tableau vide d'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
.
La tâche de sélection des sources d'information est assignée à la deuxième méthode - fetchSources (for country: String) -> AnyPublisher<[Source], Never>
qui est une copie sémantique exacte de la première méthode, sauf que cette fois au lieu d'articles, [Article]
nous choisirons des sources d'information [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()
}
Il nous renvoie "l'éditeur" AnyPublisher <[Source], Never>
avec une valeur sous forme de tableau de sources d'informations [Source]
et l'absence d'erreur Never
(en cas d'erreur, un tableau de sources vide est retourné [ ]
).Nous allons identifier la partie commune de ces deux méthodes, l'organiser comme une Generic
fonction fetch(_ url: URL) -> AnyPublisher<T, Error>
qui renvoie Generic
«l'éditeur» en AnyPublisher<T, Error>
fonction de 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()
}
Cela simplifiera les deux méthodes précédentes:
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()
}
Les «éditeurs» ainsi obtenus ne livrent rien tant que quelqu'un ne s'y «abonne» pas. Nous pouvons le faire lors de la conception UI
."Éditeurs" Publisher
comme View Model
dans SwiftUI
. Liste d'articles.
Maintenant un peu sur la logique de fonctionnement SwiftUI
.La SwiftUI
seule abstraction des «changements externes» auxquels ils répondent View
est les «éditeurs» Publisher
. Les «changements externes» peuvent être compris comme une minuterie Timer
, une notification avec NotificationCenter
ou votre objet modèle, qui en utilisant le protocole ObservableObject
peut être transformé en une seule «source de vérité» externe (source de vérité). Aux "éditeurs" ordinaires, tapez Timer
ou NotificationCenter
View
réagissez en utilisant la méthode onReceive (_: perform:)
. Un exemple d'utilisation de "l'éditeur" que Timer
nous présenterons plus loin dans le troisième article sur la création d'une application pour Hacker News .Dans cet article, nous allons nous concentrer sur la façon de faire notre modèle pour SwiftUI
"source de vérité" externe (source de vérité).Voyons d'abord comment les SwiftUI
«éditeurs» résultants devraient fonctionner dans un exemple spécifique d'affichage de divers types d'articles:.topHeadLines
- les dernières nouvelles, .articlesFromCategory(_ category: String)
- les nouvelles pour une catégorie spécifique, .articlesFromSource(_ source: String)
- les nouvelles pour une source d'information spécifique, .search (searchFilter: String)
- les nouvelles sélectionnées par une certaine condition.
Selon l' Endpoint
utilisateur qui choisit, nous devons mettre à jour la liste des articles articles
sélectionnés sur NewsAPI.org . Pour ce faire, nous allons créer une classe très simple ArticlesViewModel
qui implémente un protocole ObservableObject
avec trois @Published
propriétés: 
@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , ( «», View
TextField
),@Published var articles: [Article]
- ( «», NewsAPI.org, «»).
Dès que nous définissons des @Published
propriétés indexEndpoint
ou searchString
, nous pouvons commencer à les utiliser à la fois comme propriétés simples indexEndpoint
et searchString
, et comme "éditeurs" $indexEndpoint
et $searchString
.Dans une classe ArticlesViewModel
, vous pouvez non seulement déclarer les propriétés qui nous intéressent, mais également prescrire la logique métier de leur interaction. Dans ce but, lors de l'initialisation d'une instance d'une classe ArticlesViewModel
dans, init
nous pouvons créer un «abonnement» qui agira tout au long du «cycle de vie» de l'instance de la classe ArticlesViewModel
et reproduira la dépendance de la liste d'articles articles
sur l'index indexEndpoint
et la chaîne de recherche searchString
.Pour ce faire, Combine
nous étendons la chaîne des "éditeurs" $indexEndpoint
et $searchString
en sortie "éditeur"AnyPublisher<[Article], Never>
dont la valeur est une liste d'articles articles
. Ensuite, nous «nous y abonnons» en utilisant l'opérateur assign (to: \.articles, on: self)
et obtenons la liste des articles dont nous avons besoin en articles
tant @Published
que propriété de «sortie» qui définit UI
.Nous devons tirer la chaîne PAS simplement des propriétés indexEndpoint
et searchString
, à savoir des "éditeurs" $indexEndpoint
et $searchString
qui participent à la création UI
avec l'aide de SwiftUI
et nous les changerons là en utilisant les éléments de l'interface utilisateur Picker
et TextField
.Comment allons-nous procéder?Nous avons déjà une fonction dans notre arsenal fetchArticles (from: Endpoint)
qui est dans la classe NewsAPI
et renvoie un "éditeur" AnyPublisher<[Article], Never>
, selon la valeurEndpoint
, et nous ne pouvons que d'une manière ou d'une autre utiliser les valeurs des "éditeurs" $indexEndpoint
et $searchString
les transformer en argument de endpoint
cette fonction. Tout d'abord, combinez les "éditeurs" $indexEndpoint
et $searchString
. Pour ce faire, l' Combine
opérateur existe Publishers.CombineLatest
:
Pour créer un nouvel «éditeur» sur la base des données reçues de l'ancien «éditeur» Combine
, l'opérateur est utilisé flatMap
:
Ensuite, nous «souscrivons» à cet «éditeur» nouvellement reçu à l'aide d'un «abonné» très simple assign (to: \.articles, on: self)
et attribuons le reçu de « éditeur "valeur pour le @Published
tableau articles
:
Nous venons de créer un init( )
" éditeur "ASYNCHRONE et" souscrit "à lui, à la suite deAnyCancellable
«Abonnement» et cela est facile à vérifier si nous gardons notre «abonnement» dans une constante let subscription
:
La propriété principale d'un AnyCancellable
«abonnement» est que dès qu'il quitte son périmètre, la mémoire qu'il occupe est automatiquement libérée. Par conséquent, dès qu'il sera init( )
terminé, cet «abonnement» sera supprimé ARC
, et sans avoir le temps d'attribuer les informations asynchrones reçues avec un délai à la baie articles
. L'information asynchrone n'a tout simplement nulle part où «atterrir», dans son sens littéral, «la terre est passée de sous ses pieds».Pour enregistrer un tel «abonnement», il est nécessaire de créer une init()
variable AU-DELÀ de l'initialiseur var cancellableSet
qui enregistrera notre AnyCancellable
«abonnement» dans cette variable tout au long du «cycle de vie» de l'instance de classe ArticlesViewMode
. Par conséquent, nous supprimons la constante let subscription
et nous nous souvenons de notre AnyCancellable
«abonnement» dans la variable cancellableSet
à l'aide de l'opérateur .store ( in: &self.cancellableSet)
:
«Abonnement» à l '«éditeur» ASYNCHRONE que nous avons créé dans init( )
sera conservé tout au long du «cycle de vie» de l'instance de classe ArticlesViewModel
.Nous pouvons changer arbitrairement le sens de «éditeurs» $indexEndpoint
et / ou searchString
, et toujours grâce à la «souscription» créée, nous aurons un éventail d'articles correspondant aux valeurs de ces deux éditeurs articles
sans aucun effort supplémentaire. Cette ObservableObject
classe est généralement appelée View Model
.Afin de réduire le nombre d'appels au serveur lors de la saisie d'une chaîne de recherche searchString
, nous ne devons pas utiliser «l'éditeur» de la chaîne de recherche elle-même $searchString
, et sa version modifiée validString
:
Maintenant que nous avons View Model
pour nos articles, commençons à créer l'interface utilisateur ( UI
). Pour SwiftUI
se synchroniser View
avec le ObservableObject
modèle, une @ObservedObject
variable est utilisée qui fait référence à une instance de la classe de ce modèle. C'est cette paire - la ObservableObject
classe et la @ObservedObject
variable qui fait référence à l'instance de cette classe - qui contrôle le changement dans l'interface utilisateur ( UI
) dans SwiftUI
.Nous ajoutons à la structure une ContentView
instance de la classe ArticleViewModel
sous la forme d'une variable var articleViewModel
et la remplaçons Text ("Hello, World!")
par une liste d'articles ArticlesList
dans laquelle nous plaçons les articles articlesViewModel.articles
obtenus de notre View Model
. En conséquence, nous obtenons une liste d'articles pour un index fixe et par défaut indexEndpoint = 0
, c'est-à-dire pour les .topHeadLines
dernières nouvelles:
Ajoutez un UI
élément à notre écran pour contrôler quel ensemble d'articles nous voulons afficher. Nous utiliserons le Picker
changement d'index $articlesViewModel.indexEndpoint
. La présence d'un symbole est $
obligatoire, car cela signifie une modification de la valeur fournie par @Published
"l'éditeur". La «souscription» à cet «éditeur» est déclenchée immédiatement, que nous avons initiée init ()
, la «sortie» @Published
«l'éditeur» articles
changera et nous verrons une liste d'articles différente à l'écran:
De cette façon, nous pouvons recevoir des tableaux d'articles pour les trois options - «topHeadLines», «recherche "Et" de la catégorie ":
... mais pour une chaîne de recherche fixe et par défaut searchString = "sports"
(là où elle est requise):
Cependant, pour l'option, "search"
vous devez fournir à l'utilisateur un champ de texte SearchView
pour entrer la chaîne de recherche:
Par conséquent, l'utilisateur peut rechercher n'importe quelle actualité par la chaîne de recherche tapée:
Pour l'option, "from category"
il est nécessaire de fournir l'utilisateur la possibilité de choisir une catégorie et nous commençons par catégorie science
:
par conséquent, l'utilisateur peut rechercher des nouvelles de la catégorie choisie de nouvelles - science
, health
, business
, technology
:
on peut voir comment un simple ObservableObject
modèle qui a deux contrôlées par l' utilisateur @Published
caractéristiques - indexEndpoint
etsearchString
- vous permet de sélectionner un large éventail d'informations sur le site Web NewsAPI.org .Liste des sources d'information
Voyons comment fonctionnera SwiftUI
«l'éditeur» de sources d'informations reçues dans la classe NewsAPI fetchSources (for country: String) -> AnyPublisher<[Source], Never>
.Nous obtiendrons une liste de sources d'informations pour différents pays:
... et la possibilité de les rechercher par nom:
... ainsi que des informations détaillées sur la source sélectionnée: son nom, catégorie, pays, brève description et lien vers le site:
Si vous cliquez sur le lien, nous irons sur le site Web de ce source d'information.Pour que tout cela fonctionne, vous avez besoin d'un ObservableObject
modèle extrêmement simple qui n'a que deux @Published
propriétés contrôlées par l'utilisateur - searchString
et country
:
Et encore une fois, nous utilisons le même schéma: lors de l'initialisation d'une instance d'une classe de SourcesViewModel
classe dans init
nous créons un «abonnement» qui fonctionnera tout au long du «cycle de vie» de l'instance de classe SourcesViewModel
et nous assurerons que la liste des sources d'informations dépend sources
du pays country
et de la chaîne de recherche searchString
.Avec l'aide, Combine
nous tirons la chaîne des "éditeurs" $searchString
et $country
de la sortie "éditeur" AnyPublisher<[Source], Never>
, dont la valeur est une liste de sources d'information. Nous «nous y abonnons» en utilisant l'opérateur assign (to: \.sources, on: self)
, nous obtenons la liste des sources d'information dont nous avons besoin sources
. et rappelez-vous AnyCancellable
"l'abonnement" reçu dans une variable en cancellableSet
utilisant l'opérateur .store ( in: &self.cancellableSet)
.Maintenant que nous avons View Model
pour nos sources d'informations, commençons à créer UI
. B SwiftUI
pour synchroniser View
cObservableObject
Le modèle utilise une @ObservedObject
variable qui fait référence à une instance de la classe de ce modèle.Ajoutez l' ContentViewSources
instance de classe à la structure SourcesViewModel
sous la forme d'une variable var sourcesViewModel
, supprimez Text ("Hello, World!")
et placez la vôtre View
pour chacune des 3 @Published
propriétés sourcesViewModel
: - zone de texte
SearchView
pour la barre de recherche searchString
, -
Picker
pour le pays country
, - liste des
SourcesList
sources d'information
En conséquence, nous obtenons ce dont nous avons besoin View
:
Sur cet écran, nous gérons uniquement la chaîne de recherche en utilisant la zone de texte SearchView
et le «pays» avec Picker
, et le reste se fait AUTOMATIQUEMENT.La liste des sources d'informations SourcesList
contient un minimum d'informations sur chaque source - le nom source.name
et une brève description source.description
:
... mais elle vous permet d'obtenir des informations plus détaillées sur la source sélectionnée en utilisant le lien NavigationLink
dans lequel destination
nous indiquons DetailSourceView
quelles données source sont la source d'informations source
et l'instance de classe requise ArticlesViewModel
, ce qui permet obtenir une liste de ses articles articles
:
Voyez avec quelle élégance nous obtenons la liste des articles pour la source d'information sélectionnée dans la liste des sources SourcesList
. Notre vieil ami nous aide - une classe ArticlesViewModel
pour laquelle nous devons définir les deux @Published
propriétés «d'entrée» :- index
indexEndpoint = 3
, c'est-à-dire une option .articlesFromSource (_source:String)
correspondant à la sélection d'articles pour une source fixe source
, - chaîne
searchString
comme source elle-même (ou plutôt son identifiant) source.id
:
En général, si vous regardez l'intégralité de l'application NewsApp , vous ne verrez nulle part que nous demandons explicitement une sélection d'articles ou de sources d'informations sur le site Web NewsAPI.org . Nous ne gérons que des @Published
données, mais View Model
faisons notre travail: sélectionne les articles et les sources d'information dont nous avons besoin.Téléchargez l'image UIImage
pour l'article Article
.
Le modèle de l'article Article
contient une URL
image urlToImage
qui l' accompagne :
Sur cette base, URL
nous devrons à l'avenir obtenir les images elles-mêmes UIImage
sur le site Web NewsAPI.org .Nous connaissons déjà cette tâche. Dans la classe ImageLoader
, en utilisant la fonction, fetchImage(for url: URL?) -> AnyPublisher<UIImage?, Never>
créez un «éditeur» AnyPublisher<UIImage?, Never>
avec la valeur de l'image UIImage?
et aucune erreur Never
(en fait, si des erreurs se produisent, l'image est retournée nil
). Vous pouvez vous «abonner» à cet «éditeur» pour recevoir des images UIImage?
lors de la conception de l'interface utilisateur ( UI
). Les données source pour la fonction fetchImage(for url: URL?)
est url
, que nous avons:
Examinons en détail comment se déroule la formation avec l'aide de Combine
"l'éditeur" AnyPublisher <UIImage?, Never>
, si nous savons url
:- si
url
égal nil
, retournez Just(nil)
, - basé sur la
url
forme de "l'éditeur" dataTaskPublisher(for:)
, dont la valeur de sortie Output
est un tuple (data: Data, response: URLResponse)
et une erreur Failure
- URLError
, - nous ne prenons que les données
map {}
du tuple (data: Data, response: URLResponse)
pour un traitement ultérieur data
et la forme UIImage
, - si l'erreur de retour des étapes précédentes se produit
nil
, - nous fournissons le résultat au
main
flux, car nous supposons une utilisation ultérieure dans la conception UI
, - «Effacez» le TYPE de «l'éditeur» et renvoyez la copie
AnyPublisher
.
Vous voyez que le code est assez compact et bien lisible, il n'y en a pas callbacks
.Commençons à créer View Model
pour l'image UIImage?
. Il s'agit d'une classe ImageLoader
qui implémente le protocole ObservableObject
, avec deux @Published
propriétés:@Published url: URL?
sont des URL
images@Published var image: UIImage?
est l'image elle-même de NewsAPI.org :
Et encore une fois, lors de l'initialisation d'une instance de la classe, ImageLoader
nous devons étirer la chaîne de l'entrée "éditeur" $url
à la sortie "éditeur" AnyPublisher<UIImage?, Never>
, à laquelle nous nous "abonnerons" plus tard et obtiendrons l'image dont nous avons besoin image
:
nous utilisons l'opérateur flatMap
et un "abonné" très simple assign (to: \image, on: self)
pour l'assigner aux reçus de "l'éditeur" "Valeurs de la propriété @Published image
:
Et encore dans la variable " souscription "est cancellableSet
stockée en AnyCancellable
utilisant l'opérateur store(in: &self.cancellableSet)
.La logique de ce «téléchargeur d'images» est que vous téléchargez une image à partir de quelque chose d'autre que celle à nil URL
condition qu'elle ne précharge pas, c'est-à-direimage == nil
. Si pendant le processus de téléchargement une erreur est détectée, l'image sera absente, c'est-à-dire qu'elle image
restera égale nil
.Dans SwiftUI
nous montrons l'image avec l'aide ArticleImage
qu'une instance de la imageLoader
classe utilise pour cela ImageLoader
. Si l'image de son image n'est pas égale nil
, alors elle est affichée en utilisant Image (...)
, mais si elle est égale nil
, alors en fonction de ce qu'elle est égale url
, soit rien n'est affiché EmptyView()
, soit un rectangle Rectangle
avec un texte rotatif T s'affiche ext("Loading...")
:
Cette logique fonctionne très bien pour le cas quand vous savez avec certitude que pour url
, autre que nil
vous obtenez une image image
, comme c'est le cas avec la base de données de films TMDb . Avec NewsAPI.org, l' agrégateur de nouvelles est différent. Les articles de certaines sources d'information en donnent une différente de l' nil URL
image, mais l'accès est fermé, et nous obtenons un rectangle Rectangle
avec du texte en rotation Text("Loading...")
qui ne sera jamais remplacé:
Dans cette situation, si l' URL
image est différente de nil
, alors l'égalité de l' nil
image image
peut signifier que l'image se charge , et le fait qu'une erreur s'est produite lors du chargement et que nous n'obtiendrons jamais d'image image
. Afin de distinguer ces deux situations, nous en ajoutons une de plus ImageLoader
aux deux @Published
propriétés existantes de la classe : @Published var noData = false
- il s'agit d'une valeur booléenne avec laquelle nous indiquerons l'absence de données d'image due à une erreur lors de la sélection:
Lors de la création d'un "abonnement", nous init
interceptons toutes les erreurs Error
qui se produisent lors du chargement de l'image et accumulons leur présence dans la @Published
propriété self.noData = true
. Si le téléchargement a réussi, nous obtenons l'image image
. Nous créons le«Publisher» AnyPublisher<UIImage?, Error>
sur la base de la url
fonction fetchImageErr (for url: URL?)
:
Nous commençons à créer une méthode fetchImageErr
en initialisant le «publisher» Future
, qui peut être utilisé pour obtenir de manière asynchrone une seule valeur TYPE à l' Result
aide d'une fermeture. La fermeture a un paramètre - Promise
qui est une fonction de TYPE (Result<Output, Failure>) → Void
: Nous transformerons le
résultat Future
enAnyPublisher <UIImage?, Error>
avec l'aide de l'opérateur "Erase TYPE" eraseToAnyPublisher()
.Ensuite, nous allons effectuer les étapes suivantes, en prenant en compte toutes les erreurs possibles (nous ne nommerai pas les erreurs, il est tout simplement important pour nous de savoir qu'il ya une erreur):0. vérification url
pour nil
et noData
sur true
: le cas échéant, puis renvoyer l'erreur, sinon, le transfert url
plus par chaîne,1. créez un "éditeur" dataTaskPublisher(for:)
dont l'entrée est - url
, et la valeur de sortie Output
est un tuple (data: Data, response: URLResponse)
et une erreur URLError
,2. analysez en utilisant le tryMap { }
tuple résultant (data: Data, response: URLResponse)
: s'il response.statusCode
est dans la plage 200...299
, alors pour un traitement ultérieur, nous prenons uniquement les données data
. Sinon, nous "jetons" une erreur (quoi qu'il arrive),3. nous map { }
convertirons les données data
en UIImage
,4. livrons le résultat au main
flux, car nous supposons que nous les utiliserons plus tard dans la conception UI
- nous nous «abonnons» à «l'éditeur» reçu en utilisant sink
ses fermetures receiveCompletion
et receiveValue
,- 5. si nous receiveCompletion
trouvons une erreur dans la fermeture error
, nous signalons en l'utilisant promise (.failure(error)))
,- 6. dans la fermeture, receiveValue
nous vous informons de la réception réussie d'un tableau d'articles en utilisant promise (.success($0))
,7. nous nous souvenons de l '«abonnement» reçu dans la variable cancellableSet
pour assurer sa viabilité pendant la «durée de vie» de l'instance de classe ImageLoader
,8. nous «effaçons» le «éditeur» TYPE et renvoyer l'instance AnyPublisher
.Nous revenons à l' ArticleImage
endroit où nous allons utiliser la nouvelle @Published
variable noData
. S'il n'y a pas de données d'image, nous n'afficherons rien, c'est-à-dire EmptyView ()
:
Enfin, nous regrouperons toutes nos possibilités d'affichage des données de l' agrégateur d' actualités NewsAPI.org dans TabView
:
Afficher les erreurs lors de la récupération et du décodage des données JSON à partir du serveur NewsAPI.org .
Lors de l'accès au serveur NewsAPI.org , des erreurs peuvent se produire, par exemple, du fait que vous avez spécifié la mauvaise clé API-key
ou, ayant un tarif développeur qui ne coûte rien, a dépassé le nombre autorisé de demandes ou autre chose. Dans le même temps, le serveur NewsAPI.org vous fournit le HTTP
code et le message correspondant:
Il est nécessaire de gérer ce type d'erreur de serveur. Sinon, l'utilisateur de votre application sera dans une situation où soudainement, sans raison, le serveur NewsAPI.org cessera de traiter toutes les demandes, laissant l'utilisateur complètement perdu avec un écran vide.Jusqu'à présent, lors de la sélection d'articles [Article]
et de sources d'informations [Source]
sur le serveur NewsAPI.org nous avons ignoré toutes les erreurs et, en cas d'apparition, nous avons renvoyé des tableaux vides [Article]()
et par conséquent [Source]()
.Pour commencer avec la gestion des erreurs, fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
créons NewsAPI
une autre méthode dans la classe basée sur la méthode de sélection d'articles existante fetchArticlesErr (from endpoint: Endpoint) -> AnyPublisher<[Article], NewsError>
qui renverra non seulement un tableau d'articles [Article]
, mais aussi une erreur possible NewsError
:func fetchArticlesErr(from endpoint: Endpoint) ->
AnyPublisher<[Article], NewsError> {
. . . . . . . .
}
Cette méthode, ainsi que la méthode fetchArticles
, accepte endpoint
et renvoie le «publisher» à l'entrée avec une valeur sous la forme d'un tableau d'articles [Article]
, mais au lieu de l'absence d'erreur Never
, nous pouvons avoir une erreur définie par l'énumération NewsError
:
Commençons par créer une nouvelle méthode en initialisant le «publisher» Future
, qui peut être utiliser pour obtenir de manière asynchrone une seule valeur TYPE à l' Result
aide d'une fermeture. La fermeture a un paramètre - Promise
qui est une fonction de TYPE (Result<Output, Failure>) -> Void
: Nous transformerons le
reçu Future
en "éditeur" dont nous avons besoin en AnyPublisher <[Article], NewsError>
utilisant l'opérateur "TYPE Erase" eraseToAnyPublisher()
.Plus loin dans la nouvelle méthode, fetchArticlesErr
nous allons répéter toutes les étapes que nous avons suivies dans la méthode fetchArticles
, mais nous prendrons en compte toutes les erreurs possibles:
- 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
.
Il convient de noter que l '"éditeur" dataTaskPublisher(for:)
diffère de son prototype dataTask
en ce qu'en cas d'erreur de serveur lorsqu'il n'est response.statusCode
pas dans la plage 200...299
, il fournit toujours la valeur réussie sous la forme d'un tuple (data: Data, response: URLResponse)
, et non d'une erreur dans le formulaire (Error, URLResponse?)
. Dans ce cas, les informations réelles sur l'erreur du serveur sont contenues dans data
. "Publisher" dataTaskPublisher(for:)
délivre une erreur URLError
si une erreur se produit côté client (impossibilité de contacter le serveur, interdiction du système de sécurité ATS
, etc.).Si nous voulons afficher les erreurs dans SwiftUI
, alors nous avons besoin de celle correspondante View Model
, que nous appellerons ArticlesViewModelErr
:
Dans la classe ArticlesViewModelErr
qui implémente le protocole ObservableObject
, cette fois nous avons QUATRE @Published
propriétés:@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , Endpoint
: «» , ( «», View
), -
@Published var articles: [Article]
- ( «», NewsAPI.org ) -
@Published var articlesError: NewsError?
- , NewsAPI.org .
Lorsque vous initialisez une instance d'une classe ArticlesViewModelErr
, nous devons à nouveau étendre une chaîne à partir des "éditeurs" d'entrée $indexEndpoint
et $searchString
de la sortie de "l'éditeur" AnyPublisher<[Article],NewsError>
, à laquelle nous avons "signé" avec "l'abonné" sink
et nous obtenons beaucoup d'articles articles
ou d'erreur articlesError
.Dans notre classe, NewsAPI
nous avons déjà construit une fonction fetchArticlesErr (from endpoint: Endpoint)
qui renvoie un «éditeur» AnyPublisher<[Article], NewsError>
, en fonction de la valeur endpoint
, et nous avons seulement besoin d'utiliser en quelque sorte les valeurs des «éditeurs» $indexEndpoint
et $searchString
de les transformer en argument pour cette fonction endpoint
. Pour commencer, nous allons combiner les "éditeurs" $indexEndpoint
et $searchString
. Pour ce faire, Combine
il existe un opérateur Publishers.CombineLatest
:
Ensuite, nous devons définir le type d'erreur TYPE "éditeur" égal au requis NewsError
:
Ensuite, nous voulons utiliser la fonction fetchArticlesErr (from endpoint: Endpoint)
de notre classe NewsAPI
. Comme d'habitude, nous le ferons avec l'aide d'un opérateur flatMap
qui crée un nouvel «éditeur» sur la base des données reçues de l'ancien «éditeur»:
Ensuite, nous «souscrivons» à cet «éditeur» nouvellement reçu avec l'aide d'un «abonné» sink
et utilisons ses fermetures receiveCompletion
et receiveValue
afin de recevoir de "l'éditeur" soit la valeur d'un tableau d'articles articles
soit une erreur articlesError
:
Naturellement, il faut se souvenir de "l'abonnement" résultant dans une init()
variable externe cancellableSet
. Sinon, nous ne pourrons pas obtenir la valeur de manière asynchronearticles
ou une erreur articlesError
après la fin init()
:
Afin de réduire le nombre d'appels au serveur lors de la saisie d'une chaîne de recherche searchString
, nous ne devons pas utiliser l '"éditeur" de la barre de recherche elle $searchString
- même , mais sa version modifiée validString
:
"S'abonner" à l' "éditeur" ASYNCHRONE que nous avons créé init( )
sera persister tout au long du «cycle de vie» de l'instance de classe ArticlesViewModelErr
:
nous procédons à la correction de la nôtre UI
afin d'y afficher d'éventuelles erreurs d'échantillonnage de données. Dans SwiftU
I, dans la structure existante, nous ContentVieArticles
utilisons un autre, juste obtenu View Model
, en ajoutant simplement les lettres «Err» dans le nom. Ceci est une instance de la classe. ArticlesViewModelErr
, qui «capture» l'erreur de sélection et / ou de décodage des données d'article du serveur NewsAPI.org :
Et nous ajoutons également l'affichage d'un message d'urgence Alert
en cas d'erreur.Par exemple, si la mauvaise clé API est:struct APIConstants {
static let apiKey: String = "API_KEY"
. . . . . . . . . . . . .
}
... alors nous recevrons le message suivant:
Si la limite de demandes a été épuisée, nous recevrons le message suivant:
Revenant à la méthode de sélection des articles [Article]
avec une erreur possible NewsError
, nous pouvons simplifier son code si nous utilisons Generic
"l'éditeur" AnyPublisher<T,NewsError>,
qui, basé sur l'ensemble, url
reçoit des JSON
informations de manière asynchrone , le place directement dans le Codable
modèle T
et signale une erreur NewsError
:
comme nous le savons, ce code est très facile à utiliser pour obtenir un «éditeur» spécifique si les données source url
sont un agrégateur deEndpoint
nouvelles NewsAPI.org ou un pays country
source d'information, et la sortie nécessite différents modèles - par exemple, une liste d'articles ou de sources d'information:

Conclusion
Nous avons appris à quel point il est facile de répondre aux HTTP
demandes avec l'aide de Combine
son URLSession
«éditeur» dataTaskPublisher
et Codable
. Si vous n'avez pas besoin de suivre les erreurs, vous obtenez un Generic
code à 5 lignes très simple pour "l'éditeur" AnyPublisher<T, Never>
, qui reçoit de manière asynchrone les JSON
informations et les place directement dans le Codable
modèle en T
fonction des données données url
:
ce code est très facile à utiliser pour obtenir un éditeur spécifique, si les données source url
sont, par exemple Endpoint
, et la sortie nécessite différents modèles - par exemple, un ensemble d'articles ou une liste de sources d'informations.Si vous devez prendre en compte les erreurs, le code de Generic
"l'éditeur" sera un peu plus compliqué, mais ce sera quand même un code très simple sans aucun rappel:
En utilisant la technologie d'exécution des HTTP
requêtes à l'aide Combine
, pouvez-vous créer un «éditeur» AnyPublisher<UIImage?, Never>
qui sélectionne de manière asynchrone les données et reçoit une image UIImage? basé sur URL
. Les ImageLoade
téléchargeurs d' images r sont mis en mémoire cache pour éviter la récupération répétée de données asynchrones.Toutes sortes de «éditeurs» obtenus peuvent très facilement être «mis au travail» dans les classes ObservableObject, qui utilisent leurs propriétés @Published pour contrôler votre interface utilisateur conçue à l'aide de SwiftUI. Ces classes jouent généralement le rôle du modèle d'affichage, car elles ont les propriétés @Published dites «d'entrée» qui correspondent aux éléments d'interface utilisateur actifs (TextField, Stepper, zones de texte Picker, boutons radio Toggle, etc.) et les propriétés @Published «de sortie». , constitué principalement d'éléments d'interface utilisateur passifs (textes, images, formes géométriques Circle (), Rectangle (), etc.).Cette idée imprègne l'ensemble de l' application d' agrégateur d'actualités NewsAPI.org présentée dans cet article. assez universel et a été utilisé lorsquedévelopper une application pour la base de données de films TMDb et l'agrégateur de nouvelles Hacker News , qui seront discutées dans de futurs articles.Le code d'application de cet article est sur Github .PS1. Je veux attirer votre attention sur le fait que si vous utilisez le simulateur pour l'application présentée dans cet article, sachez que NavigationLink
le simulateur fonctionne avec une erreur. Vous pouvez utiliserNavigationLink
sur le simulateur une seule fois. Ceux. vous avez utilisé le lien, êtes revenu en arrière, cliquez sur le même lien - et rien ne se passe. Tant que vous n'utiliserez pas un autre lien, le premier ne fonctionnera pas, mais le second deviendra inaccessible. Mais cela n'est observé que sur le simulateur, sur un vrai appareil, tout fonctionne bien.2. Certaines sources d'information utilisent encore à la http
place https
des "images" de leurs articles. Si vous voulez vraiment voir ces «images», mais ne pouvez pas contrôler la source de leur apparence, vous devez configurer le système de sécurité ATS ( App Transport Security)
pour recevoir ces http
«images», mais ce n'est bien sûr pas une bonne idée . Vous pouvez utiliser des options plus sécurisées .Références:
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» )