La consulta HTTP
es una de las habilidades más importantes que debe adquirir al desarrollar iOS
aplicaciones. En versiones anteriores Swift
(antes de la versión 5), independientemente de si generó estas solicitudes desde cero o si utilizó el conocido marco de Alamofire , terminó con un código complejo y confuso del callback
tipo completionHandler: @escaping(Result<T, APIError>) -> Void
.La aparición en Swift 5
el nuevo marco de la programación reactiva funcional Combine
en conjunto con la existente URLSession
, y le Codable
proporciona todas las herramientas necesarias para la escritura independiente de código muy compacto para obtener datos de Internet.En este artículo, de acuerdo con el concepto, Combine
crearemos "editores"Publisher
para seleccionar datos de Internet, a los que podemos "suscribirnos" fácilmente en el futuro y utilizarlos al diseñar UI
con UIKit
y con ayuda SwiftUI
.Como SwiftUI
parece más conciso y más efectivo, porque la acción de los "editores" Publisher
no se limita solo a datos de muestra, y se extiende aún más hasta el control de la interfaz de usuario ( UI
). El hecho es que SwiftUI
la separación de datos se View
lleva a cabo utilizando ObservableObject
clases con @Published
propiedades, cuyos cambios se SwiftUI
monitorean AUTOMÁTICAMENTE y se redibujan completamente View
.En estas ObservableObject
clases, puede simplemente poner una cierta lógica comercial de la aplicación, si alguna de estas@Published
propiedades son el resultado de síncronos y / o asíncronos de transformación otras @Published
propiedades que se pueden cambiar directamente tales elementos "activos" de la interfaz de usuario ( UI
) como cuadros de texto TextField
, Picker
, Stepper
, Toggle
etc.Para aclarar lo que está en juego, daré ejemplos específicos. Ahora, muchos servicios como NewsAPI.org y Hacker News ofrecen agregadores de noticias para ofrecer a los usuarios elegir diferentes conjuntos de artículos según lo que les interese. En el caso del agregador de noticias NewsAPI.org, pueden ser las últimas noticias o noticias en alguna categoría: "deporte", "salud", "ciencia", "tecnología", "negocios" o noticias de una fuente de información específica "CNN" , ABC news, Bloomberg, etc. El usuario generalmente "expresa" sus deseos de servicios en la forma Endpoint
que le sea necesaria URL
.Entonces, usando el marco Combine
, puedesObservableObject
clases que utilizan un código muy compacto (en la mayoría de los casos, no más de 10-12 líneas) una vez para formar una dependencia síncrona y / o asíncrona de la lista de artículos Endpoint
en forma de "suscripción" de @Published
propiedades "pasivas" a propiedades "activas" @Published
. Esta "suscripción" será válida durante todo el "ciclo de vida" de la instancia de ObservableObject
clase. Y luego SwiftUI
, le dará al usuario la capacidad de administrar solo las @Published
propiedades "activas" en el formulario Endpoint
, es decir, QUÉ quiere ver: si serán artículos con las últimas noticias o artículos en la sección "salud". La aparición de los propios artículos con las últimas noticias o artículos en la sección "salud" UI
será proporcionada AUTOMÁTICAMENTE por estas ObservableObject
clases y sus propiedades "pasivas" publicadas. En codigoSwiftUI
nunca necesitará solicitar explícitamente una selección de artículos, porque las ObservableObject
clases que juegan un papel son responsables de su visualización correcta y sincrónica en la pantalla View Model
.Te mostraré cómo funciona esto con NewsAPI.org y Hacker News y la base de datos de películas TMDb en una serie de artículos. En los tres casos, funcionará aproximadamente el mismo patrón de uso Combine
, porque en aplicaciones de este tipo siempre debe crear LISTAS de películas o artículos, elija las "IMÁGENES" (imágenes) que las acompañan, BUSQUE en las bases de datos de las películas o artículos necesarios utilizando la barra de búsqueda.Al acceder a dichos servicios, pueden producirse errores, por ejemplo, debido a que especificó la clave incorrecta API-key
o excedió el número permitido de solicitudes u otra cosa. Debe manejar este tipo de error, de lo contrario corre el riesgo de dejar al usuario completamente perdido con una pantalla en blanco. Por lo tanto, debe poder no solo seleccionar Combine
datos de Internet usando , sino también informar errores que pueden ocurrir durante el muestreo y controlar su apariencia en la pantalla.Comenzaremos a desarrollar nuestra estrategia desarrollando una aplicación que interactúe con el agregador de noticias NewsAPI.org . Debo decir que en esta aplicación se SwiftUI
utilizará en un grado mínimo sin adornos y únicamente para mostrar cómo Combine
con sus "editores"Publisher
y "suscripción" se Subscription
ven afectados UI
.Se recomienda que se registre en el sitio web NewsAPI.org y reciba la clave API
que se requiere para completar cualquier solicitud al servicio NewsAPI.org . Debe colocarlo en el archivo NewsAPI.swift en la estructura APIConstants
.El código de aplicación para este artículo está en Github .Modelo de datos del servicio NewsAPI.org y API
El servicio NewsAPI.org le permite seleccionar información sobre artículos de noticias actuales [Article]
y sus fuentes [Source]
. Nuestro modelo de datos será muy simple, se encuentra en el archivo 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?
}
El artículo Article
contendrá el identificador id
, título title
, descripción description
, autor author
, URL de la "imagen" urlToImage
, fecha de publicación publishedAt
y fuente de publicación source
. Encima de los artículos [Article]
hay un complemento NewsResponse
en el que solo nos interesará la propiedad articles
, que es una variedad de artículos. La estructura raíz NewsResponse
y la estructura Article
son Codable
, lo que nos permitirá literalmente dos líneas de código que decodifican los JSON
datos en el Modelo. La estructura Article
debe ser también Identifiable
, si queremos que sea más fácil para nosotros para mostrar una gran variedad de artículos [Article]
como una lista List
en SwiftUI
. El protocolo Identifiable
requiere la presencia de una propiedadid
que le proporcionaremos un identificador único artificial UUID()
.La fuente de información Source
contendrá un identificador id
, nombre name
, descripción description
, país country
, categoría de fuente de publicación category
, URL del sitio url
. Encima de las fuentes de información [Source]
hay un complemento SourcesResponse
en el que solo nos interesará una propiedad sources
, que es una variedad de fuentes de información. La estructura raíz SourcesResponse
y la estructura Source
son Codable
, lo que nos permitirá decodificar muy fácilmente los JSON
datos en un modelo. La estructura Source
también debería ser Identifiable
, si queremos facilitar la visualización de una serie de fuentes de información [Source]
en forma de una lista List
enSwiftUI
. El protocolo Identifiable
requiere la presencia de la propiedad id
que ya tenemos, por lo que no se requerirá ningún esfuerzo adicional de nuestra parte.Ahora considere lo que necesitamos API
para el servicio NewsAPI.org y colóquelo en el archivo NewsAPI.swift . La parte central de la nuestra API
es una clase NewsAPI
que presenta dos métodos para seleccionar datos del agregador de noticias NewsAPI.org : artículos [Article]
y fuentes de información [Source]
:fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
- selección de artículos en [Article]
función del parámetro endpoint
,fetchSources (for country: String) -> AnyPublisher<[Source], Never>
- Una selección de fuentes de información [Source]
para un país en particular country
.
Estos métodos devuelven no solo una serie de artículos [Article]
o una variedad de fuentes de información [Source]
, sino los "editores" correspondientes del Publisher
nuevo marco Combine
. Ambos editores no devuelven ningún error, Never
y si todavía se produce un error de muestreo o codificación, [Article]()
se devuelve una matriz vacía de artículos o fuentes de información [Source]()
sin ningún mensaje de por qué estas matrices estaban vacías. Qué artículos o fuentes de información queremos seleccionar del servidor NewsAPI.org , indicaremos utilizando la enumeraciónenum 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"
}
}
}
Eso:- las últimas noticias
.topHeadLines
, - noticias de cierta categoría (deportes, salud, ciencia, negocios, tecnología)
.articlesFromCategory(_ category: String)
, - noticias de una fuente de información específica (CNN, ABC News, Fox News, etc.)
.articlesFromSource(_ source: String)
, - cualquier noticia
.search (searchFilter: String)
que cumpla una determinada condición searchFilter
, - fuentes de información
.sources (country:String)
para un país en particular country
.
Para facilitar la inicialización de la opción que necesitamos, agregamos un Endpoint
inicializador init?
a la enumeración para varias listas de artículos y fuentes de información dependiendo del índice index
y la cadena text
, que tiene diferentes significados para diferentes opciones de enumeración: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
}
}
Volvamos a la clase NewsAPI
y consideremos con más detalle el primer método fetchArticles (from endpoint: Endpoint)-> AnyPublisher<[Article], Never>
, que selecciona artículos [Article]
basados en el parámetro endpoint
y no devuelve ningún 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()
}
- sobre la base del
endpoint
formulario URL
para la solicitud, la lista de artículos deseada endpoint.absoluteURL
, si esto no se puede hacer, devuelva una matriz vacía de artículos[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 tarea de seleccionar fuentes de información se asigna al segundo método, fetchSources (for country: String) -> AnyPublisher<[Source], Never>
que es una copia semántica exacta del primer método, excepto que esta vez, en lugar de artículos [Article]
, elegiremos las fuentes de información [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()
}
Nos devuelve el "editor" AnyPublisher <[Source], Never>
con un valor en forma de una matriz de fuentes de información [Source]
y la ausencia de un error Never
(en caso de errores, se devuelve una matriz vacía de fuentes [ ]
).Seleccionaremos la parte común de estos dos métodos, organícela como una Generic
función fetch(_ url: URL) -> AnyPublisher<T, Error>
que devuelve el Generic
"editor" en AnyPublisher<T, Error>
función 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()
}
Esto simplificará los dos métodos anteriores:
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()
}
Los "editores" así obtenidos no entregan nada hasta que alguien se "suscribe" a ellos. Podemos hacer esto al diseñar UI
."Editores" Publisher
como View Model
en SwiftUI
. Listado de artículos.
Ahora un poco sobre la lógica del funcionamiento SwiftUI
.La SwiftUI
única abstracción de los "cambios externos" a los que responden View
son los "editores" Publisher
. Por "cambios externos" puede comprender un temporizador Timer
, una notificación NotificationCenter
o su objeto Modelo, que mediante el protocolo ObservableObject
puede convertirse en una única "fuente de verdad" externa. Para los "editores" comunes, escriba Timer
o NotificationCenter
View
reaccione utilizando el método onReceive (_: perform:)
. Un ejemplo del uso del "editor" Timer
lo presentaremos más adelante en el tercer artículo sobre la creación de una aplicación para Hacker News .En este artículo, nos centraremos en cómo hacer nuestro modelo para SwiftUI
"fuente de verdad" externa (fuente de verdad).Veamos primero cómo SwiftUI
deberían funcionar los "editores" resultantes en un ejemplo específico de visualización de varios tipos de artículos:.topHeadLines
- las últimas noticias, .articlesFromCategory(_ category: String)
- noticias para una categoría específica, .articlesFromSource(_ source: String)
- noticias para una fuente de información específica, .search (searchFilter: String)
- noticias seleccionadas por una determinada condición.
Según el Endpoint
usuario que elija, necesitamos actualizar la lista de artículos articles
seleccionados de NewsAPI.org . Para hacer esto, crearemos una clase muy simple ArticlesViewModel
que implemente un protocolo ObservableObject
con tres @Published
propiedades: 
@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , ( «», View
TextField
),@Published var articles: [Article]
- ( «», NewsAPI.org, «»).
Tan pronto como nos propusimos @Published
propiedades indexEndpoint
o searchString
, podemos empezar a usarlos tanto como propiedades simples indexEndpoint
y searchString
, y como "editores" $indexEndpoint
y $searchString
.En una clase ArticlesViewModel
, no solo puede declarar las propiedades que nos interesan, sino también prescribir la lógica comercial de su interacción. Para este propósito, al inicializar una instancia de una clase ArticlesViewModel
en, init
podemos crear una "suscripción" que actuará durante todo el "ciclo de vida" de la instancia de la clase ArticlesViewModel
y reproducirá la dependencia de la lista de artículos articles
en el índice indexEndpoint
y la cadena de búsqueda searchString
.Para hacer esto, Combine
ampliamos la cadena de "editores" $indexEndpoint
y la $searchString
salida "editor"AnyPublisher<[Article], Never>
cuyo valor es una lista de artículos articles
. Luego nos "suscribimos" a él utilizando el operador assign (to: \.articles, on: self)
y obtenemos la lista de artículos que necesitamos articles
como una @Published
propiedad de "salida" que define UI
.Debemos extraer la cadena NO simplemente de las propiedades indexEndpoint
y searchString
, es decir, de los "editores" $indexEndpoint
y $searchString
que participan en la creación UI
con la ayuda de SwiftUI
y los cambiaremos allí utilizando los elementos de la interfaz de usuario Picker
y TextField
.¿Como haremos esto?Ya tenemos una función en nuestro arsenal fetchArticles (from: Endpoint)
que está en la clase NewsAPI
y devuelve un "editor" AnyPublisher<[Article], Never>
, según el valorEndpoint
, y solo podemos de alguna manera usar los valores de los "publicadores" $indexEndpoint
y $searchString
convertirlos en un argumento para endpoint
esta función. Primero, combine los "editores" $indexEndpoint
y $searchString
. Para hacer esto, el Combine
operador existe Publishers.CombineLatest
:
para crear un nuevo "editor" basado en los datos recibidos del "editor" anterior Combine
, se utiliza el operador flatMap
: a
continuación, nos "suscribimos" a este "editor" recién recibido utilizando un "suscriptor" muy simple assign (to: \.articles, on: self)
y asignamos lo recibido de " editor "valor para la @Published
matriz articles
:
acabamos de crear un init( )
" editor "ASINCRÓNICO y" suscrito "a él, como resultado deAnyCancellable
"Suscripción" y esto es fácil de verificar si mantenemos nuestra "suscripción" en una constante let subscription
:
la propiedad principal de una AnyCancellable
"suscripción" es que tan pronto como abandona su alcance, la memoria ocupada se libera automáticamente. Por lo tanto, tan pronto como se init( )
complete, esta "suscripción" se eliminará ARC
, sin tener tiempo para asignar la información asincrónica recibida con un retraso de tiempo a la matriz articles
. La información asincrónica simplemente no tiene ningún lugar para "aterrizar", en su sentido literal, "la tierra ha pasado de debajo de sus pies".Para guardar dicha "suscripción", es necesario crear una init()
variable MÁS ALLÁ del inicializador var cancellableSet
que guardará nuestra AnyCancellable
"suscripción" en esta variable durante todo el "ciclo de vida" de la instancia de clase ArticlesViewMode
. Por lo tanto, eliminamos la constante let subscription
y recordamos nuestra AnyCancellable
"suscripción" en la variable cancellableSet
usando el operador .store ( in: &self.cancellableSet)
:
"Suscripción" al "editor" ASINCRÓNICO que creamos init( )
se conservará durante todo el "ciclo de vida" de la instancia de clase ArticlesViewModel
.Podemos cambiar arbitrariamente el significado de "editores" $indexEndpoint
y / o searchString
, y siempre gracias a la "suscripción" creada, tendremos una serie de artículos correspondientes a los valores de estos dos editores articles
sin ningún esfuerzo adicional. Esta ObservableObject
clase generalmente se llama View Model
.Para reducir el número de llamadas al servidor al escribir una cadena de búsqueda searchString
, no debemos usar el "editor" de la cadena de búsqueda en sí $searchString
, y su versión modificada validString
:
ahora que tenemos View Model
para nuestros artículos, comencemos a crear la interfaz de usuario ( UI
). Para SwiftUI
sincronizar View
con el ObservableObject
Modelo, @ObservedObject
se usa una variable que se refiere a una instancia de la clase de este Modelo. Es este par, la ObservableObject
clase y la @ObservedObject
variable que hace referencia a la instancia de esta clase, que controlan el cambio en la interfaz de usuario ( UI
) en SwiftUI
.Agregamos a la estructura una ContentView
instancia de la clase ArticleViewModel
en forma de variable var articleViewModel
y la reemplazamos Text ("Hello, World!")
con una lista de artículos ArticlesList
en la que colocamos los artículos articlesViewModel.articles
obtenidos de nuestro View Model
. Como resultado, obtenemos una lista de artículos para un índice fijo y predeterminado indexEndpoint = 0
, es decir, para las .topHeadLines
últimas noticias:
agregue un UI
elemento a nuestra pantalla para controlar qué conjunto de artículos queremos mostrar. Utilizaremos el Picker
cambio de índice $articlesViewModel.indexEndpoint
. La presencia de un símbolo es $
obligatoria, ya que esto significa un cambio en el valor suministrado por el @Published
"editor". La "suscripción" a este "editor" se activa de inmediato, que iniciamos en init ()
, el " @Published
editor" de "salida" articles
cambiará y veremos una lista diferente de artículos en la pantalla: por lo
tanto, podemos recibir conjuntos de artículos para las tres opciones: "topHeadLines", "buscar "Y" de la categoría ":
... pero para una cadena de búsqueda fija y predeterminada searchString = "sports"
(donde se requiere):
Sin embargo, para la opción, "search"
debe proporcionar al usuario un campo de texto SearchView
para ingresar la cadena de búsqueda:
Como resultado, el usuario puede buscar cualquier noticia por la cadena de búsqueda escrita:
para la opción, "from category"
es necesario proporcionar al usuario la oportunidad de elegir una categoría y que se inicia con la categoría science
:
como resultado, el usuario puede buscar cualquier noticia de la categoría elegida de noticias - science
, health
, business
, technology
:
podemos ver cómo un simple ObservableObject
modelo que tiene dos controlados por el usuario @Published
características - indexEndpoint
ysearchString
- le permite seleccionar una amplia gama de información del sitio web NewsAPI.org .Lista de fuentes de información.
Veamos cómo funcionará el SwiftUI
"editor" de las fuentes de información recibidas en la clase NewsAPI fetchSources (for country: String) -> AnyPublisher<[Source], Never>
.Obtendremos una lista de fuentes de información para diferentes países:
... y la capacidad de buscarlos por nombre:
... así como información detallada sobre la fuente seleccionada: su nombre, categoría, país, breve descripción y enlace al sitio:
si hace clic en el enlace, iremos al sitio web de este Fuente de información.Para que todo esto funcione, necesita un ObservableObject
Modelo extremadamente simple que tenga solo dos @Published
propiedades controladas por el usuario , searchString
y country
:
Y nuevamente, usamos el mismo esquema: al inicializar una instancia de una clase de una SourcesViewModel
clase en init
creamos una "suscripción" que operará durante todo el "ciclo de vida" de la instancia de clase SourcesViewModel
y nos aseguraremos de que la lista de fuentes de información dependa sources
del país country
y la cadena de búsqueda searchString
.Con la ayuda Combine
, extraemos la cadena de los "editores" $searchString
y $country
de la salida "editor" AnyPublisher<[Source], Never>
, cuyo valor es una lista de fuentes de información. Nos "suscribimos" a él utilizando el operador assign (to: \.sources, on: self)
, obtenemos la lista de fuentes de información que necesitamos sources
. y recuerde la AnyCancellable
"suscripción" recibida en una variable cancellableSet
usando el operador .store ( in: &self.cancellableSet)
.Ahora que tenemos View Model
para nuestras fuentes de información, comencemos a crear UI
. B SwiftUI
para sincronizar View
cObservableObject
El modelo usa una @ObservedObject
variable que se refiere a una instancia de la clase de este modelo.Agregue la ContentViewSources
instancia de clase a la estructura SourcesViewModel
en forma de variable var sourcesViewModel
, elimine Text ("Hello, World!")
y coloque la suya propia View
para cada una de las 3 @Published
propiedades sourcesViewModel
: - cuadro de texto
SearchView
para la barra de búsqueda searchString
, -
Picker
para el país country
, - lista de
SourcesList
fuentes de información
Como resultado, obtenemos lo que necesitamos View
:
en esta pantalla, solo administramos la cadena de búsqueda usando el cuadro de texto SearchView
y el "país" con Picker
, y el resto sucede AUTOMÁTICAMENTE.La lista de fuentes de información SourcesList
contiene información mínima sobre cada fuente: el nombre source.name
y una breve descripción source.description
:
... pero le permite obtener información más detallada sobre la fuente seleccionada utilizando el enlace NavigationLink
en el que destination
indicamos DetailSourceView
qué datos de origen es la fuente de información source
y la instancia deseada de la clase ArticlesViewModel
, lo que permite obtener una lista de sus artículos articles
:
Vea cuán elegantemente obtenemos la lista de artículos para la fuente seleccionada de fuente de información en la lista de fuentes SourcesList
. Nuestro viejo amigo nos ayuda: una clase ArticlesViewModel
para la que debemos establecer ambas @Published
propiedades de "entrada" :- índice
indexEndpoint = 3
, es decir, una opción .articlesFromSource (_source:String)
correspondiente a la selección de artículos para una fuente fija source
, - cadena
searchString
como la fuente misma (o más bien su identificador) source.id
:
En general, si mira toda la aplicación NewsApp , no verá en ningún lado que solicitemos explícitamente una selección de artículos o fuentes de información del sitio web NewsAPI.org . Solo gestionamos @Published
datos, pero View Model
hacemos nuestro trabajo: selecciona los artículos y las fuentes de información que necesitamos.Descargar imagen UIImage
para artículo Article
.
El modelo del artículo Article
contiene una URL
imagen urlToImage
que lo acompaña : en
base a esto, URL
en el futuro debemos obtener las imágenes UIImage
del sitio web NewsAPI.org .Ya estamos familiarizados con esta tarea. En la clase ImageLoader
, utilizando la función, fetchImage(for url: URL?) -> AnyPublisher<UIImage?, Never>
cree un "editor" AnyPublisher<UIImage?, Never>
con el valor de la imagen UIImage?
y sin errores Never
(de hecho, si se producen errores, se devuelve la imagen nil
). Puede "suscribirse" a este "editor" para recibir imágenes UIImage?
al diseñar la interfaz de usuario ( UI
). La fuente de datos para la función fetchImage(for url: URL?)
es url
, que tenemos:
Consideremos en detalle cómo se lleva a cabo la formación con la ayuda del Combine
"editor" AnyPublisher <UIImage?, Never>
, si sabemos url
:- Si
url
la igualdad nil
, el retorno Just(nil)
, - en base a la
url
forma de la "editor" dataTaskPublisher(for:)
, cuyo valor de salida Output
es una tupla (data: Data, response: URLResponse)
y un error Failure
- URLError
, - tomamos solo datos
map {}
de la tupla (data: Data, response: URLResponse)
para su posterior procesamiento data
y forma UIImage
, - si se produce el error de devolución de los pasos anteriores
nil
, - entregamos el resultado a la
main
transmisión, ya que suponemos un mayor uso en el diseño UI
, - "Borre" el TIPO del "editor" y devuelva la copia
AnyPublisher
.
Verá que el código es bastante compacto y bien legible, no hay ninguno callbacks
.Comencemos a crear View Model
para la imagen UIImage?
. Esta es una clase ImageLoader
que implementa el protocolo ObservableObject
, con dos @Published
propiedades:@Published url: URL?
son URL
imágenes@Published var image: UIImage?
es la imagen misma de NewsAPI.org :
Y nuevamente, al inicializar una instancia de la clase, ImageLoader
debemos estirar la cadena desde el "editor" de entrada hasta el "editor" $url
de salida AnyPublisher<UIImage?, Never>
, al cual nos "suscribiremos" más tarde y obtendremos la imagen que necesitamos image
:
utilizamos el operador flatMap
y un "suscriptor" muy simple assign (to: \image, on: self)
para asignarlo al recibido del "editor" "Valores de la propiedad @Published image
:
Y nuevamente en la variable " suscripción "se cancellableSet
almacena AnyCancellable
utilizando el operador store(in: &self.cancellableSet)
.La lógica de este "descargador de imágenes" es que descargue una imagen de algo diferente a la que nil URL
siempre que no se haya precargado, es decirimage == nil
. Si durante el proceso de descarga se detecta algún error, la imagen estará ausente, es decir, image
permanecerá igual nil
.En SwiftUI
, mostramos la imagen con la ayuda ArticleImage
que una instancia de la imageLoader
clase utiliza para esto ImageLoader
. Si la imagen de su imagen no es igual nil
, entonces se muestra usando Image (...)
, pero si es igual nil
, dependiendo de a qué es igual url
, o no se muestra nada EmptyView()
, o se muestra un rectángulo Rectangle
con texto rotativo T ext("Loading...")
:
esta lógica funciona bien para el caso cuando sabe con certeza que para url
, además de nil
obtener una imagen image
, como es el caso con la base de datos de películas TMDb . Con NewsAPI.org, el agregador de noticias es diferente. Los artículos de algunas fuentes de información dan una diferente de la nil URL
imagen, pero el acceso a ella está cerrado y obtenemos un rectángulo Rectangle
con texto giratorio Text("Loading...")
que nunca será reemplazado:
en esta situación, si la URL
imagen es diferente de nil
, entonces la igualdad de la nil
imagen image
puede significar que la imagen se está cargando , y el hecho de que ocurrió un error durante la carga y nunca obtendremos una imagen image
. Para distinguir entre estas dos situaciones, agregamos una más ImageLoader
a las dos @Published
propiedades existentes en la clase : @Published var noData = false
- este es un valor booleano con el que indicaremos la ausencia de datos de imagen debido a un error durante la selección:
al crear una "suscripción", init
detectamos todos los errores Error
que ocurren al cargar la imagen y acumulamos su presencia en la @Published
propiedad self.noData = true
. Si la descarga fue exitosa, obtenemos la imagen image
. Creamos el"Editor" AnyPublisher<UIImage?, Error>
sobre la base de la url
función fetchImageErr (for url: URL?)
:
Comenzamos a crear un método fetchImageErr
inicializando el "editor" Future
, que puede usarse para obtener de forma asincrónica un único valor de TIPO Result
utilizando un cierre. El cierre tiene un parámetro, Promise
que es una función de TYPE (Result<Output, Failure>) → Void
: convertiremos el
resultado Future
enAnyPublisher <UIImage?, Error>
con la ayuda del operador "Erase TYPE" eraseToAnyPublisher()
.A continuación, se llevará a cabo los pasos siguientes, teniendo en cuenta todos los errores posibles (no vamos a identificar los errores, es simplemente importante para nosotros saber que hay un error):0. cheque url
para nil
y noData
sobre true
: si es así, a continuación, devolver el error, si no, la transferencia url
aún más por cadena,1. cree un "publicador" dataTaskPublisher(for:)
cuya entrada sea - url
, y el valor de salida Output
sea una tupla (data: Data, response: URLResponse)
y un error URLError
,2. analice usando la tryMap { }
tupla resultante (data: Data, response: URLResponse)
: si response.statusCode
está en el rango 200...299
, entonces para el procesamiento posterior tomamos solo los datos data
. De lo contrario, "arrojamos" un error (no importa qué),3. map { }
transformamos los datos data
en UIImage
,4. entregamos el resultado a la main
secuencia, ya que suponemos que lo usaremos más adelante en el proceso de diseño UI
- nos "suscribimos" al "editor" recibido usando sink
sus cierres receiveCompletion
y receiveValue
,- 5. si receiveCompletion
encontramos un error en el cierre error
, informamos usándolo promise (.failure(error)))
,- 6. en el cierre, receiveValue
informamos sobre la recepción exitosa de una serie de artículos usando promise (.success($0))
,7. recordamos la "suscripción" recibida en la variable cancellableSet
para asegurar su viabilidad dentro de la "vida útil" de la instancia de clase ImageLoader
,8. "borramos" el TIPO "editor" y devolver la instancia AnyPublisher
.Regresamos a ArticleImage
donde usaremos la nueva @Published
variable noData
. Si no hay datos de imagen, entonces no mostraremos nada, es decir EmptyView ()
:
Finalmente, agruparemos todas nuestras posibilidades de mostrar datos del agregador de noticias NewsAPI.org en TabView
:
Mostrar errores al recuperar y decodificar datos JSON del servidor NewsAPI.org .
Al acceder al servidor NewsAPI.org , pueden producirse errores , por ejemplo, debido a que especificó la clave incorrecta API-key
o, al tener una tarifa de desarrollador que no cuesta nada, excedió el número permitido de solicitudes u otra cosa. Al mismo tiempo, el servidor NewsAPI.org le proporciona el HTTP
código y el mensaje correspondiente:
es necesario manejar este tipo de error del servidor. De lo contrario, el usuario de su aplicación caerá en una situación en la que de repente, sin ninguna razón, el servidor NewsAPI.org dejará de procesar cualquier solicitud, dejando al usuario completamente perdido con una pantalla en blanco.Hasta ahora, al seleccionar artículos [Article]
y fuentes de información [Source]
del servidor NewsAPI.org ignoramos todos los errores y, en caso de que aparecieran, devolvimos matrices vacías [Article]()
y como resultado [Source]()
.Comenzando con el manejo de errores, creemos otro método fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
en la clase basado en el NewsAPI
método de selección de artículos existente fetchArticlesErr (from endpoint: Endpoint) -> AnyPublisher<[Article], NewsError>
que devolverá no solo una serie de artículos [Article]
, sino también un posible error NewsError
:func fetchArticlesErr(from endpoint: Endpoint) ->
AnyPublisher<[Article], NewsError> {
. . . . . . . .
}
Este método, así como el método fetchArticles
, acepta endpoint
y devuelve el "editor" en la entrada con un valor en forma de una matriz de artículos [Article]
, pero en lugar de la ausencia de un error Never
, podemos tener un error definido por la enumeración NewsError
:
comencemos a crear un nuevo método inicializando el "editor" Future
, que puede ser use para obtener asincrónicamente un solo valor de TYPE Result
usando un cierre. El cierre tiene un parámetro, Promise
que es una función de TYPE (Result<Output, Failure>) -> Void
: convertiremos lo
recibido Future
en el "editor" que necesitamos AnyPublisher <[Article], NewsError>
usando el operador "TYPE Erase" eraseToAnyPublisher()
.Además en el nuevo método, fetchArticlesErr
repetiremos todos los pasos que tomamos en el método fetchArticles
, pero tendremos en cuenta todos los posibles errores:
- 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
.
Cabe señalar que el "editor" dataTaskPublisher(for:)
difiere de su prototipo dataTask
en que en caso de un error del servidor cuando response.statusCode
NO está en el rango 200...299
, aún entrega el valor exitoso en forma de tupla (data: Data, response: URLResponse)
, y no un error en el formulario (Error, URLResponse?)
. En este caso, la información de error real del servidor está contenida en data
. El "editor" dataTaskPublisher(for:)
entrega un error URLError
si ocurre un error en el lado del cliente (incapacidad para contactar al servidor, prohibición del sistema de seguridad ATS
, etc.).Si queremos mostrar errores SwiftUI
, entonces necesitamos el correspondiente View Model
, al que llamaremos ArticlesViewModelErr
:
En la clase ArticlesViewModelErr
que implementa el protocolo ObservableObject
, esta vez tenemos CUATRO @Published
propiedades:@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , Endpoint
: «» , ( «», View
), -
@Published var articles: [Article]
- ( «», NewsAPI.org ) -
@Published var articlesError: NewsError?
- , NewsAPI.org .
Al inicializar una instancia de una clase ArticlesViewModelErr
, de nuevo debemos extender una cadena de entrada "editores" $indexEndpoint
y $searchString
la salida "editor" AnyPublisher<[Article],NewsError>
, a la que "firmado" con el "Suscriptor" sink
y obtener una gran cantidad de artículos articles
o de error articlesError
.En nuestra clase, NewsAPI
ya hemos construido una función fetchArticlesErr (from endpoint: Endpoint)
que devuelve un "publicador" AnyPublisher<[Article], NewsError>
, dependiendo del valor endpoint
, y solo necesitamos de alguna manera usar los valores de los "publicadores" $indexEndpoint
y $searchString
convertirlos en un argumento para esta función endpoint
. Para empezar, combinaremos los "editores" $indexEndpoint
y $searchString
. Para hacer esto, Combine
hay un operador Publishers.CombineLatest
:
Luego debemos establecer el tipo de error TIPO "editor" igual al requerido NewsError
:
A continuación, queremos usar la función fetchArticlesErr (from endpoint: Endpoint)
de nuestra clase NewsAPI
. Como de costumbre, haremos esto con la ayuda de un operador flatMap
que crea un nuevo "editor" sobre la base de los datos recibidos del "editor" anterior:
luego "suscribimos" a este "editor" recién recibido con la ayuda de un "suscriptor" sink
y usamos sus cierres receiveCompletion
y receiveValue
para recibir del "editor" el valor de una serie de artículos articles
o errores articlesError
:
Naturalmente, es necesario recordar la "suscripción" resultante en alguna init()
variable externa cancellableSet
. De lo contrario, no podremos obtener el valor de forma asincrónicaarticles
o un error articlesError
después de la finalización init()
:
para reducir el número de llamadas al servidor al escribir una cadena de búsqueda searchString
, no debemos usar el "editor" de la barra de búsqueda en sí $searchString
, sino su versión modificada validString
:
"Suscribirse" al "editor" ASINCRÓNICO que creamos init( )
será persisten durante todo el "ciclo de vida" de la instancia de clase ArticlesViewModelErr
:
procedemos a la corrección de la nuestra UI
para mostrar posibles errores de muestreo de datos en ella. En SwiftU
I, en la estructura existente, ContentVieArticles
usamos otro, recién obtenido View Model
, simplemente agregando las letras "Err" en el nombre. Esta es una instancia de la clase. ArticlesViewModelErr
, que "detecta" el error de seleccionar y / o decodificar los datos del artículo del servidor NewsAPI.org :
y también agregamos la visualización de un mensaje de emergencia Alert
en caso de error.Por ejemplo, si la clave API incorrecta es:struct APIConstants {
static let apiKey: String = "API_KEY"
. . . . . . . . . . . . .
}
... entonces recibiremos el siguiente mensaje:
Si el límite de solicitudes se ha agotado, recibiremos el siguiente mensaje:
Volviendo al método de selección de artículos [Article]
con un posible error NewsError
, podemos simplificar su código si usamos el Generic
"editor" AnyPublisher<T,NewsError>,
que, según el conjunto, url
recibe JSON
información de forma asincrónica , la coloca directamente en el Codable
Modelo T
e informa un error NewsError
:
Como sabemos, este código es muy fácil de usar para obtener un "editor" específico si la fuente de datos url
es un agregador deEndpoint
noticias o país NewsAPI.orgcountry
fuente de información, y la salida requiere varios modelos, por ejemplo, una lista de artículos o fuentes de información:

Conclusión
Aprendimos lo fácil que es cumplir HTTP
con las solicitudes con la ayuda de Combine
su URLSession
"editor" dataTaskPublisher
y Codable
. Si no necesita realizar un seguimiento de los errores, obtiene un Generic
código de 5 líneas muy simple para el "editor" AnyPublisher<T, Never>
, que recibe JSON
información de forma asíncrona y la coloca directamente en el Codable
Modelo en T
función de lo dado url
:
Este código es muy fácil de usar para obtener un editor específico, si los datos de origen url
son, por ejemplo Endpoint
, y la salida requiere varios modelos, por ejemplo, un conjunto de artículos o una lista de fuentes de información.Si necesita tener en cuenta los errores, el código para el Generic
"editor" será un poco más complicado, pero aún así será un código muy simple sin devoluciones de llamada:
Usando la tecnología de ejecución de HTTP
consultas Combine
, ¿puede crear un "editor" AnyPublisher<UIImage?, Never>
que seleccione datos asincrónicamente y reciba una imagen UIImage? basado en URL
. Los ImageLoade
descargadores de imágenes r se almacenan en memoria caché para evitar la recuperación de datos asincrónicos repetidos.Todo tipo de "publicadores" obtenidos se pueden "hacer funcionar" muy fácilmente en las clases de ObservableObject, que usan sus propiedades @Published para controlar su IU diseñada usando SwiftUI. Estas clases generalmente desempeñan el papel del modelo de vista, ya que tienen las llamadas propiedades de "entrada" @Published que corresponden a los elementos activos de la interfaz de usuario (TextField, Stepper, cuadros de texto del selector, botones de radio de alternancia, etc.) y "output" @Published propiedades , que consiste principalmente en elementos de interfaz de usuario pasivos (textos de texto, imágenes de imágenes, formas geométricas de círculo (), rectángulo (), etc.Esta idea impregna toda la aplicación de agregador de noticias NewsAPI.org presentada en este artículo. Resultó ser bastante universal y se usó cuandodesarrollando una aplicación para la base de datos de películas TMDb y el agregador de noticias Hacker News , que se discutirá en futuros artículos.El código de aplicación para este artículo está en Github .PD1. Quiero llamar su atención sobre el hecho de que si usa el simulador para la aplicación presentada en este artículo, sepa que NavigationLink
el simulador funciona con un error. Puedes usarNavigationLink
en el simulador solo 1 vez. Aquellos. usaste el enlace, volviste, haz clic en el mismo enlace y no pasa nada. Hasta que use otro enlace, el primero no funcionará, pero el segundo será inaccesible. Pero esto solo se observa en el simulador, en un dispositivo real todo funciona bien.2. Algunas fuentes de información todavía se utilizan http
en su lugar https
para "imágenes" de sus artículos. Si definitivamente desea ver estas "imágenes", pero no puede controlar la fuente de su apariencia, debe configurar el sistema de seguridad ATS ( App Transport Security)
para recibir estas http
"imágenes", pero esto, por supuesto, no es una buena idea . Puede usar opciones más seguras .Referencias
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» )