A consulta HTTP
é uma das habilidades mais importantes que você precisa obter ao desenvolver iOS
aplicativos. Nas versões anteriores Swift
(até a versão 5), independentemente de você ter gerado essas solicitações "do zero" ou usando a conhecida estrutura Alamofire , você acabou com um código de callback
tipo complexo e confuso completionHandler: @escaping(Result<T, APIError>) -> Void
.A aparência na Swift 5
nova estrutura da programação reativa funcional Combine
em conjunto com a existente URLSession
e Codable
fornece a você todas as ferramentas necessárias para a gravação independente de códigos muito compactos para buscar dados da Internet.Neste artigo, de acordo com o conceito, Combine
criaremos "editores"Publisher
para selecionar dados da Internet, nos quais podemos "facilmente assinar" no futuro e usar ao projetar UI
com UIKit
e com ajuda SwiftUI
.Como SwiftUI
parece mais conciso e mais eficaz, porque a ação dos "editores" Publisher
não se limita apenas aos dados de amostra e se estende até o controle da interface do usuário ( UI
). O fato é que SwiftUI
a separação de dados é View
realizada usando ObservableObject
classes com @Published
propriedades, cujas alterações são SwiftUI
monitoradas AUTOMATICAMENTE e redesenhadas completamente View
.Nessas ObservableObject
classes, você pode simplesmente colocar uma certa lógica comercial do aplicativo, se alguns deles@Published
propriedades são o resultado de síncronos e / ou assíncronos de transformação outras @Published
propriedades que podem ser alterados directamente tais elementos "activos" da interface de utilizador ( UI
) como caixas de texto TextField
, Picker
, Stepper
, Toggle
etc.Para deixar claro o que está em jogo, darei exemplos específicos. Agora, muitos serviços, como NewsAPI.org e Hacker News, oferecem agregadores de notícias para oferecer aos usuários a escolha de diferentes conjuntos de artigos, dependendo do que lhes interessa. No caso do agregador de notícias NewsAPI.org, podem ser as últimas notícias ou notícias de alguma categoria - "esporte", "saúde", "ciência", "tecnologia", "negócios" ou notícias de uma fonte de informação específica "CNN" , Notícias da ABC, Bloomberg, etc. O usuário geralmente "expressa" seus desejos por serviços na forma Endpoint
que for necessária URL
.Então, usando a estrutura Combine
, você podeObservableObject
classes usando um código muito compacto (na maioria dos casos, não mais que 10 a 12 linhas) uma vez para formar uma dependência síncrona e / ou assíncrona da lista de artigos Endpoint
na forma de uma "assinatura" de @Published
propriedades "passivas" para propriedades "ativas" @Published
. Essa "assinatura" será válida durante todo o "ciclo de vida" da instância da ObservableObject
classe. E então, SwiftUI
você dará ao usuário a oportunidade de gerenciar apenas as @Published
propriedades "ativas" no formulário Endpoint
, ou seja, O QUE ele deseja ver: se serão artigos com as últimas notícias ou artigos na seção "saúde". A aparência dos artigos em si, com as últimas notícias ou artigos na seção "saúde", UI
será fornecida AUTOMATICAMENTE por essas ObservableObject
classes e suas propriedades @ passivas "publicadas". Em códigoSwiftUI
você nunca precisará solicitar explicitamente uma seleção de artigos, porque as ObservableObject
classes que desempenham um papel são responsáveis por sua exibição correta e síncrona na tela View Model
.Vou mostrar como isso funciona com o NewsAPI.org e o Hacker News e o banco de dados de filmes TMDb em uma série de artigos. Nos três casos, aproximadamente o mesmo padrão de uso funcionará Combine
, porque em aplicativos desse tipo você sempre deve criar LISTAS de filmes ou artigos, escolha as “FOTOS” (imagens) que os acompanham, PESQUISA nas bases de dados dos filmes ou artigos necessários usando a barra de pesquisa.Ao acessar esses serviços, podem ocorrer erros, por exemplo, devido ao fato de você ter especificado a chave errada API-key
ou ter excedido o número permitido de solicitações ou outra coisa. Você precisa lidar com esse tipo de erro, caso contrário, corre o risco de deixar o usuário completamente perdido com uma tela em branco. Portanto, você precisa não apenas selecionar Combine
dados da Internet usando , mas também reportar erros que podem ocorrer durante a amostragem e controlar sua aparência na tela.Começaremos a desenvolver nossa estratégia, desenvolvendo um aplicativo que interaja com o agregador de notícias NewsAPI.org . Devo dizer que, neste aplicativo, ele SwiftUI
será usado no mínimo, sem frescuras e apenas para mostrar como, Combine
com seus "editores"Publisher
e "assinatura" são Subscription
afetados UI
.É recomendável que você se registre no site NewsAPI.org e receba a chave API
necessária para concluir qualquer solicitação ao serviço NewsAPI.org . Você deve colocá-lo no arquivo NewsAPI.swift na estrutura APIConstants
.O código do aplicativo para este artigo está no Github .Modelo de dados de serviço e API do NewsAPI.org
O serviço NewsAPI.org permite selecionar informações sobre os artigos de notícias atuais [Article]
e suas fontes [Source]
. Nosso modelo de dados será muito simples, está localizado no arquivo 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?
}
O artigo Article
conterá o identificador id
, título title
, descrição description
, autor author
, URL da “imagem” urlToImage
, data da publicação publishedAt
e fonte da publicação source
. Acima dos artigos, [Article]
há um complemento NewsResponse
no qual estaremos interessados apenas na propriedade articles
, que é uma variedade de artigos. A estrutura raiz NewsResponse
e a estrutura Article
são Codable
, o que nos permitirá literalmente duas linhas de código decodificando os JSON
dados no Modelo. A estrutura Article
também deve ser Identifiable
, se queremos tornar mais fácil para nós para exibir uma série de artigos [Article]
como uma lista List
em SwiftUI
. Protocolo Identifiable
requer a presença de uma propriedadeid
que forneceremos com um identificador exclusivo artificial UUID()
.A fonte de informações Source
conterá um identificador id
, nome name
, descrição description
, país country
, categoria de fonte de publicação category
, URL do site url
. Acima das fontes de informação, [Source]
há um complemento SourcesResponse
no qual estaremos interessados apenas em uma propriedade sources
, que é uma matriz de fontes de informação. A estrutura SourcesResponse
e a estrutura raiz Source
são Codable
, o que nos permitirá decodificar com muita facilidade os JSON
dados em um modelo. A estrutura Source
também deve ser Identifiable
, se quisermos facilitar a exibição de uma matriz de fontes de informação [Source]
na forma de uma lista List
emSwiftUI
. O protocolo Identifiable
requer a presença da propriedade id
que já possuímos, portanto nenhum esforço adicional será necessário de nós.Agora considere o que precisamos API
para o serviço NewsAPI.org e coloque-o no arquivo NewsAPI.swift . A parte central da nossa API
é uma classe NewsAPI
que apresenta dois métodos para selecionar dados do agregador de notícias NewsAPI.org - artigos [Article]
e fontes de informação [Source]
:fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
- seleção de artigos com [Article]
base no parâmetro endpoint
,fetchSources (for country: String) -> AnyPublisher<[Source], Never>
- Uma seleção de fontes de informação [Source]
para um país específico country
.
Esses métodos retornam não apenas uma variedade de artigos [Article]
ou uma variedade de fontes de informações [Source]
, mas os "publicadores" correspondentes da Publisher
nova estrutura Combine
. Ambos os editores não retornam nenhum erro - Never
e se ainda ocorreu um erro de amostragem ou codificação, uma matriz vazia de artigos [Article]()
ou fontes de informação é retornada [Source]()
sem nenhuma mensagem sobre por que essas matrizes estavam vazias. Quais artigos ou fontes de informação que queremos selecionar no servidor NewsAPI.org , indicaremos usando a enumeraçãoenum 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"
}
}
}
Isto:- as últimas notícias
.topHeadLines
, - notícias de uma determinada categoria (esportes, saúde, ciência, negócios, tecnologia)
.articlesFromCategory(_ category: String)
, - notícias de uma fonte específica de informações (CNN, ABC News, Fox News etc.)
.articlesFromSource(_ source: String)
, - qualquer notícia
.search (searchFilter: String)
que atenda a uma determinada condição searchFilter
, - fontes de informação
.sources (country:String)
para um país em particular country
.
Para facilitar a inicialização da opção de que precisamos, adicionamos um Endpoint
inicializador init?
à enumeração para várias listas de artigos e fontes de informações, dependendo do índice index
e da string text
, que tem significados diferentes para diferentes opções de enumeração: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
}
}
Vamos voltar à classe NewsAPI
e considerar mais detalhadamente o primeiro método fetchArticles (from endpoint: Endpoint)-> AnyPublisher<[Article], Never>
, que seleciona artigos com [Article]
base no parâmetro endpoint
e não retorna nenhum erro - 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()
}
- com base no
endpoint
formulário URL
para a solicitação, a lista desejada de artigos endpoint.absoluteURL
, se isso não puder ser feito, retorne uma matriz vazia de artigos[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
.
A tarefa de selecionar fontes de informação é atribuída ao segundo método - fetchSources (for country: String) -> AnyPublisher<[Source], Never>
que é uma cópia semântica exata do primeiro método, exceto que, desta vez, em vez de artigos [Article]
, escolheremos as fontes de informação [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()
}
Ele retorna para nós o "publicador" AnyPublisher <[Source], Never>
com um valor na forma de uma matriz de fontes de informação [Source]
e a ausência de um erro Never
(no caso de erros, uma matriz vazia de fontes é retornada [ ]
).Vamos destacar a parte comum desses dois métodos, organizá-la como uma Generic
função fetch(_ url: URL) -> AnyPublisher<T, Error>
que retorna o Generic
"editor" com AnyPublisher<T, Error>
base em 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()
}
Isso simplificará os dois 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()
}
Os "publicadores" assim obtidos não entregam nada até que alguém os "assine". Podemos fazer isso ao projetar UI
."Editores" Publisher
como View Model
em SwiftUI
. Lista de artigos.
Agora um pouco sobre a lógica do funcionamento SwiftUI
.A SwiftUI
única abstração das "mudanças externas" às quais eles respondem View
são os "editores" Publisher
. “Alterações externas” podem ser entendidas como um temporizador Timer
, uma notificação NotificationCenter
ou seu objeto Modelo, que usando o protocolo ObservableObject
pode ser transformado em uma única “fonte de verdade” externa (fonte de verdade). Para "publishers" ordinários tipo Timer
ou NotificationCenter
View
reage usando o método onReceive (_: perform:)
. Um exemplo do uso do "editor" Timer
que apresentaremos posteriormente no terceiro artigo sobre a criação de um aplicativo para o Hacker News .Neste artigo, focaremos em como criar nosso modelo para SwiftUI
"fonte da verdade" externa (fonte da verdade).Vamos primeiro ver como os SwiftUI
"editores" recebidos devem funcionar em um exemplo específico de exibição de vários tipos de artigos:.topHeadLines
- as últimas notícias, .articlesFromCategory(_ category: String)
- notícias para uma categoria específica, .articlesFromSource(_ source: String)
- notícias para uma fonte específica de informações, .search (searchFilter: String)
- notícias selecionadas por uma determinada condição.
Dependendo de qual Endpoint
usuário escolher, precisamos atualizar a lista de artigos articles
selecionados no NewsAPI.org . Para fazer isso, criaremos uma classe muito simples ArticlesViewModel
que implementa um protocolo ObservableObject
com três @Published
propriedades: 
@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , ( «», View
TextField
),@Published var articles: [Article]
- ( «», NewsAPI.org, «»).
Assim que definir @Published
propriedades indexEndpoint
ou searchString
, podemos começar a usá-los tanto como propriedades simples indexEndpoint
e searchString
, e como "editores" $indexEndpoint
e $searchString
.Em uma classe ArticlesViewModel
, você pode não apenas declarar as propriedades de interesse para nós, mas também prescrever a lógica de negócios de sua interação. Para esse propósito, ao inicializar uma instância de uma classe ArticlesViewModel
, init
podemos criar uma "assinatura" que atuará durante todo o "ciclo de vida" da instância da classe ArticlesViewModel
e reproduzirá a dependência da lista de artigos articles
no índice indexEndpoint
e na cadeia de pesquisa searchString
.Para fazer isso, Combine
estendemos a cadeia de "editores" $indexEndpoint
e produzimos $searchString
"editores"AnyPublisher<[Article], Never>
cujo valor é uma lista de artigos articles
. Em seguida, "assinamos" usando o operador assign (to: \.articles, on: self)
e obtemos a lista de artigos que precisamos articles
como uma @Published
propriedade "output" que define UI
.Nós devemos puxar a cadeia NÃO simplesmente das propriedades indexEndpoint
e searchString
, principalmente dos "editores" $indexEndpoint
e $searchString
que participam da criação UI
com a ajuda de, SwiftUI
e nós os alteraremos lá usando os elementos da interface do usuário Picker
e TextField
.Como faremos isso?Já temos uma função em nosso arsenal fetchArticles (from: Endpoint)
que está na classe NewsAPI
e retorna um "editor" AnyPublisher<[Article], Never>
, dependendo do valorEndpoint
, e só podemos usar de alguma forma os valores dos "editores" $indexEndpoint
e $searchString
transformá-los em um argumento para endpoint
essa função. Primeiro, combine os "editores" $indexEndpoint
e $searchString
. Para fazer isso, o Combine
operador existe Publishers.CombineLatest
:
Para criar um novo "editor" com base nos dados recebidos do "editor" anterior Combine
, o operador é usado flatMap
:
Em seguida, "assinamos" esse "editor" recém-recebido usando um "assinante" muito simples assign (to: \.articles, on: self)
e atribuímos o recebido de " publisher "à @Published
matriz articles
:
acabamos de criar uma init( )
" editora "ASSÍNCRONA e" assinada "a ela, como resultado deAnyCancellable
“Assinatura” e é fácil verificar se mantemos nossa “assinatura” constante let subscription
:
a principal propriedade de uma AnyCancellable
“assinatura” é que, assim que sai de seu escopo, a memória ocupada por ela é automaticamente liberada. Portanto, assim que for init( )
concluída, essa "assinatura" será excluída ARC
, sem tempo para atribuir as informações assíncronas recebidas com atraso na matriz articles
. Informações assíncronas simplesmente não têm para onde "pousar", em seu sentido literal, "a Terra se escondeu".Para salvar essa "assinatura", é necessário criar uma init()
variável ALÉM do inicializador var cancellableSet
que salvará nossa AnyCancellable
"assinatura" nessa variável durante todo o "ciclo de vida" da instância da classe ArticlesViewMode
. Portanto, removemos a constante let subscription
e lembramos da nossa AnyCancellable
"assinatura" na variável cancellableSet
usando o operador .store ( in: &self.cancellableSet)
:
"Subscription" para o "editor" ASSÍNCRONO em que criamos init( )
será preservado durante todo o "ciclo de vida" da instância da classe ArticlesViewModel
.Podemos alterar arbitrariamente o significado de "editores" $indexEndpoint
e / ou searchString
, e sempre graças à "assinatura" criada, teremos uma variedade de artigos correspondentes aos valores desses dois editores articles
sem nenhum esforço adicional. Essa ObservableObject
classe geralmente é chamada View Model
.Para reduzir o número de chamadas ao servidor ao digitar uma string de pesquisa searchString
, não devemos usar o "editor" da própria string de pesquisa $searchString
e sua versão modificada validString
:
agora que temos View Model
nossos artigos, vamos começar a criar a interface do usuário ( UI
). Para SwiftUI
sincronizar View
com o ObservableObject
Modelo, @ObservedObject
é usada uma variável que se refere a uma instância da classe deste Modelo. É esse par - a ObservableObject
classe e a @ObservedObject
variável que referencia a instância dessa classe - que controla a alteração na interface do usuário ( UI
) em SwiftUI
.Adicionamos à estrutura uma ContentView
instância da classe ArticleViewModel
na forma de uma variável var articleViewModel
e a substituímos Text ("Hello, World!")
por uma lista de artigos ArticlesList
na qual colocamos os artigos articlesViewModel.articles
obtidos de nossa View Model
. Como resultado, obtemos uma lista de artigos para um índice fixo e padrão indexEndpoint = 0
, ou seja, para as .topHeadLines
últimas notícias:
Adicione um UI
elemento à nossa tela para controlar qual conjunto de artigos queremos exibir. Usaremos a Picker
alteração do índice $articlesViewModel.indexEndpoint
. A presença de um símbolo é $
obrigatória, pois isso significa uma alteração no valor fornecido pelo @Published
"editor". A "assinatura" deste "editor" é acionada imediatamente, e iniciámos init ()
o " @Published
editor" de "saída" articles
mudará e veremos uma lista diferente de artigos na tela:
Dessa forma, podemos receber matrizes de artigos para as três opções - "topHeadLines", "pesquisar "E" da categoria ":
... mas para uma sequência de pesquisa fixa e padrão searchString = "sports"
(onde é necessária): no
entanto, para a opção, "search"
você deve fornecer ao usuário um campo de texto SearchView
para inserir a sequência de pesquisa:
Como resultado, o usuário pode procurar qualquer notícia pela sequência de pesquisa digitada:
Para a opção, "from category"
é necessário fornecer ao usuário a oportunidade de escolher uma categoria e começamos com categoria science
:
como resultado, o usuário pode procurar qualquer notícia sobre a categoria escolhida de notícias - science
, health
, business
, technology
:
podemos ver como uma forma muito simples ObservableObject
modelo que tem duas controladas pelo usuário @Published
recursos - indexEndpoint
esearchString
- permite selecionar uma ampla variedade de informações no site NewsAPI.org .Lista de fontes de informação
Vamos ver como o SwiftUI
"editor" de fontes de informações recebidas na classe NewsAPI funcionará fetchSources (for country: String) -> AnyPublisher<[Source], Never>
.Obteremos uma lista de fontes de informações para diferentes países:
... e a capacidade de procurá-las por nome:
... bem como informações detalhadas sobre a fonte selecionada: nome, categoria, país, descrição resumida e link para o site:
Se você clicar no link, acessaremos o site deste site. fonte de informação.Para que tudo isso funcione, você precisa de um ObservableObject
Modelo extremamente simples que possua apenas duas @Published
propriedades controladas pelo usuário - searchString
e country
:
E, novamente, usamos o mesmo esquema: ao inicializar uma instância de uma classe de uma SourcesViewModel
classe em init
criamos uma “assinatura” que funcionará durante todo o “ciclo de vida” da instância da classe SourcesViewModel
e garantimos que a lista de fontes de informações dependa sources
do país country
e da string de pesquisa searchString
.Com a ajuda Combine
, extraímos a cadeia dos "editores" $searchString
e produzimos $country
"editores" AnyPublisher<[Source], Never>
, cujo valor é uma lista de fontes de informação. Nós "assinamos" usando o operador assign (to: \.sources, on: self)
, obtemos a lista de fontes de informações de que precisamos sources
. e lembre-se da AnyCancellable
"assinatura" recebida em uma variável cancellableSet
usando o operador .store ( in: &self.cancellableSet)
.Agora que temos View Model
nossas fontes de informação, vamos começar a criar UI
. B SwiftUI
para sincronizar View
cObservableObject
O modelo usa uma @ObservedObject
variável que se refere a uma instância da classe deste modelo.Adicione a ContentViewSources
instância da classe à estrutura SourcesViewModel
na forma de uma variável var sourcesViewModel
, remova Text ("Hello, World!")
e coloque a sua View
para cada uma das 3 @Published
propriedades sourcesViewModel
: - caixa de texto
SearchView
para a barra de pesquisa searchString
, -
Picker
para o país country
, - lista de
SourcesList
fontes de informação
Como resultado, obtemos o que precisamos View
:
Nesta tela, gerenciamos apenas a sequência de pesquisa usando a caixa de texto SearchView
e o “país” com Picker
, e o resto acontece automaticamente.A lista de fontes de informação SourcesList
contém informações mínimas sobre cada fonte - o nome source.name
e uma breve descrição source.description
:
... mas permite obter informações mais detalhadas sobre a fonte selecionada usando o link NavigationLink
no qual destination
indicamos DetailSourceView
quais dados de fonte são a fonte de informação source
e a instância desejada da classe ArticlesViewModel
, permitindo obtenha uma lista de seus artigos articles
:
Veja como elegantemente obtemos a lista de artigos para a fonte selecionada de fonte de informações na lista de fontes SourcesList
. Nosso velho amigo nos ajuda - uma classe ArticlesViewModel
para a qual devemos definir as duas @Published
propriedades de "entrada" :- índice
indexEndpoint = 3
, ou seja, uma opção .articlesFromSource (_source:String)
correspondente à seleção de artigos para uma fonte fixa source
, - string
searchString
como a própria fonte (ou melhor, seu identificador) source.id
:
Em geral, se você olhar para todo o aplicativo NewsApp , não verá nenhum lugar em que solicitamos explicitamente uma seleção de artigos ou fontes de informação no site NewsAPI.org . Nós gerenciamos apenas os @Published
dados, mas View Model
fazemos o nosso trabalho: seleciona os artigos e as fontes de informação que precisamos.Faça o download da imagem UIImage
para o artigo Article
.
O modelo do artigo Article
contém uma URL
imagem urlToImage
que o acompanha : com
base nisso, URL
no futuro, devemos obter as imagens UIImage
no site NewsAPI.org .Já estamos familiarizados com esta tarefa. Na classe ImageLoader
, usando a função, fetchImage(for url: URL?) -> AnyPublisher<UIImage?, Never>
crie um "editor" AnyPublisher<UIImage?, Never>
com o valor da imagem UIImage?
e sem erros Never
(na verdade, se ocorrerem erros, a imagem será retornada nil
). Você pode "assinar" este "editor" para receber imagens UIImage?
ao projetar a interface do usuário ( UI
). Os dados de origem para a função fetchImage(for url: URL?)
são url
:
Vamos considerar em detalhes como está acontecendo a formação, com a ajuda do Combine
"editor" AnyPublisher <UIImage?, Never>
, se soubermos url
:- se for
url
igual nil
, retorne Just(nil)
, - com base na
url
forma do "editor" dataTaskPublisher(for:)
, cujo valor de saída Output
é uma tupla (data: Data, response: URLResponse)
e um erro Failure
- URLError
, - coletamos apenas dados
map {}
da tupla (data: Data, response: URLResponse)
para processamento data
e formulário adicionais UIImage
, - se ocorrer o erro de retorno das etapas anteriores
nil
, - entregamos o resultado ao
main
fluxo, pois assumimos um uso adicional no design UI
, - "Apague" o TIPO do "editor" e devolva a cópia
AnyPublisher
.
Você vê que o código é bastante compacto e bem legível, não há nenhum callbacks
.Vamos começar a criar View Model
para a imagem UIImage?
. Esta é uma classe ImageLoader
que implementa o protocolo ObservableObject
, com duas @Published
propriedades:@Published url: URL?
são URL
imagens@Published var image: UIImage?
é a própria imagem do NewsAPI.org :
E, novamente, ao inicializar uma instância da classe, ImageLoader
precisamos estender a cadeia da entrada "publisher" $url
para a saída "publisher" AnyPublisher<UIImage?, Never>
, na qual assinaremos mais tarde e obteremos a imagem de que precisamos image
:
Usamos o operador flatMap
e um "assinante" muito simples assign (to: \image, on: self)
para atribuí-la ao recebido do "publicador" "Valores para a propriedade @Published image
:
E novamente na variável " assinatura "é cancellableSet
armazenada AnyCancellable
usando o operador store(in: &self.cancellableSet)
.A lógica desse “downloader de imagens” é que você baixa uma imagem de algo diferente daquele, nil URL
desde que não tenha sido pré-carregado, ou seja,image == nil
. Se durante o processo de download for detectado algum erro, a imagem estará ausente, ou seja, image
permanecerá igual nil
.Em SwiftUI
mostramos a imagem com a ajuda ArticleImage
que uma instância da imageLoader
classe usa para isso ImageLoader
. Se a imagem dele não for igual nil
, ela será exibida usando Image (...)
, mas se for igual nil
, dependendo do que for igual url
, nada será mostrado EmptyView()
ou um retângulo Rectangle
com texto rotativo T será exibido ext("Loading...")
:
Essa lógica funciona bem para o caso quando você tiver certeza de que url
, além de nil
obter uma imagem image
, como é o caso do banco de dados de filmes TMDb . Com o NewsAPI.org, o agregador de notícias é diferente. Os artigos de algumas fontes de informação dão uma diferente da nil URL
imagem, mas o acesso é fechado e obtemos um retângulo Rectangle
com texto rotativo Text("Loading...")
que nunca será substituído:
Nesta situação, se a URL
imagem for diferente nil
, a igualdade da nil
imagem image
pode significar que a imagem está sendo carregada. , e o fato de ocorrer um erro ao carregar e nunca obteremos uma imagem image
. Para distinguir entre essas duas situações, adicionamos mais uma ImageLoader
às duas @Published
propriedades existentes na classe : @Published var noData = false
- este é um valor booleano com o qual indicaremos a ausência de dados da imagem devido a um erro durante a seleção:
Ao criar uma "assinatura", init
capturamos todos os erros Error
que ocorrem ao carregar a imagem e acumulamos sua presença na @Published
propriedade self.noData = true
. Se o download foi bem-sucedido, obtemos a imagem image
. Criamos o"Publicador" AnyPublisher<UIImage?, Error>
com base na url
função fetchImageErr (for url: URL?)
:
Começamos a criar um método fetchImageErr
inicializando o "publicador" Future
, que pode ser usado para obter de forma assíncrona um único valor TYPE Result
usando um fechamento. O fechamento possui um parâmetro - Promise
que é uma função do TYPE (Result<Output, Failure>) → Void
: Transformaremos o
resultado Future
emAnyPublisher <UIImage?, Error>
com a ajuda do operador “Erase TYPE” eraseToAnyPublisher()
.Em seguida, vamos realizar os seguintes passos, tendo em conta todos os erros possíveis (não vamos identificar erros, é simplesmente importante para nós saber que há um erro):0. verificação url
de nil
e noData
em true
: em caso afirmativo, em seguida, retornar o erro, se não, a transferência de url
mais longe, cadeia,1. crie um "publicador" dataTaskPublisher(for:)
cuja entrada seja - url
e o valor de saída Output
seja uma tupla (data: Data, response: URLResponse)
e um erro URLError
,2. analise usando a tryMap { }
tupla resultante (data: Data, response: URLResponse)
: se response.statusCode
estiver no intervalo 200...299
, para processamento adicional, coletamos apenas os dados data
. Caso contrário, "descartamos" um erro (não importa o quê),3. vamos map { }
converter os dados data
para UIImage
,4. entregar o resultado para o main
fluxo, uma vez que assumimos que vamos usá-lo mais tarde no projeto UI
- que “assinar” para o “publisher” recebeu usando sink
seus fechamentos receiveCompletion
e receiveValue
,- 5. se receiveCompletion
encontrar um erro no fechamento error
, relatamos usando-o promise (.failure(error)))
,- 6. no fechamento, receiveValue
informamos sobre o recebimento bem-sucedido de uma variedade de artigos usando promise (.success($0))
,7. lembramos a "assinatura" recebida na variável cancellableSet
para garantir sua viabilidade durante o "tempo de vida" da instância da classe ImageLoader
,8. apagamos "o" editor "TYPE e retorne a instância AnyPublisher
.Retornamos para ArticleImage
onde usaremos a nova @Published
variável noData
. Se não houver dados de imagem, não exibiremos nada, ou seja EmptyView ()
:
Finalmente, reuniremos todas as nossas possibilidades de exibir dados do agregador de notícias NewsAPI.org em TabView
:
Exibir erros ao buscar e decodificar dados JSON do servidor NewsAPI.org .
Ao acessar o servidor NewsAPI.org , podem ocorrer erros , por exemplo, devido ao fato de você ter especificado a chave errada API-key
ou, por ter uma tarifa de desenvolvedor que não custa nada, exceder o número permitido de solicitações ou outra coisa. Ao mesmo tempo, o servidor NewsAPI.org fornece o HTTP
código e a mensagem correspondente:
É necessário lidar com esse tipo de erro no servidor. Caso contrário, o usuário do seu aplicativo entrará em uma situação em que, repentinamente, sem motivo, o servidor NewsAPI.org interromperá o processamento de quaisquer solicitações, deixando o usuário completamente perdido com uma tela em branco.Até agora, ao selecionar artigos [Article]
e fontes de informações [Source]
no servidor NewsAPI.org ignoramos todos os erros e, no caso de sua aparência, retornamos matrizes vazias [Article]()
e, como resultado [Source]()
.Começando com o tratamento de erros, vamos fetchArticles (from endpoint: Endpoint) -> AnyPublisher<[Article], Never>
criar NewsAPI
outro método na classe com base no método de seleção de artigo existente fetchArticlesErr (from endpoint: Endpoint) -> AnyPublisher<[Article], NewsError>
que retornará não apenas uma matriz de artigos [Article]
, mas também um possível erro NewsError
:func fetchArticlesErr(from endpoint: Endpoint) ->
AnyPublisher<[Article], NewsError> {
. . . . . . . .
}
Esse método, assim como o método fetchArticles
, aceita endpoint
e retorna o "publicador" na entrada com um valor na forma de uma matriz de artigos [Article]
, mas, em vez da ausência de um erro Never
, podemos ter um erro definido pela enumeração NewsError
:
Começamos a criar um novo método inicializando o "publicador" Future
, que use para obter assincronamente um único valor TYPE Result
usando um fechamento. O fechamento possui um parâmetro - Promise
que é uma função do TYPE (Result<Output, Failure>) -> Void
: Transformaremos os
recebidos Future
no "editor" necessário, AnyPublisher <[Article], NewsError>
usando o operador "TYPE Erase" eraseToAnyPublisher()
.Mais adiante no novo método, fetchArticlesErr
repetiremos todas as etapas que executamos no método fetchArticles
, mas consideraremos todos os erros possíveis:
- 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
.
Deve-se observar que o "publicador" dataTaskPublisher(for:)
difere de seu protótipo dataTask
, pois, no caso de um erro do servidor quando response.statusCode
NÃO está no intervalo 200...299
, ele ainda fornece o valor bem-sucedido na forma de uma tupla (data: Data, response: URLResponse)
, e não um erro no formulário (Error, URLResponse?)
. Nesse caso, as informações reais de erro do servidor estão contidas data
. O "publicador" dataTaskPublisher(for:)
gera um erro URLError
se ocorrer um erro no lado do cliente (incapacidade de entrar em contato com o servidor, proibição do sistema de segurança ATS
etc.).Se queremos exibir erros SwiftUI
, precisamos do correspondente View Model
, que chamaremos ArticlesViewModelErr
:
Na classe ArticlesViewModelErr
que implementa o protocolo ObservableObject
, desta vez temos QUATRO @Published
propriedades:@Published var indexEndpoint: Int
— Endpoint
( «», View
), @Published var searchString: String
— , Endpoint
: «» , ( «», View
), -
@Published var articles: [Article]
- ( «», NewsAPI.org ) -
@Published var articlesError: NewsError?
- , NewsAPI.org .
Quando você inicializa uma instância de uma classe ArticlesViewModelErr
, devemos estender novamente uma cadeia de entrada "editores" $indexEndpoint
e $searchString
saída "publisher" AnyPublisher<[Article],NewsError>
, à qual "assinamos" com o "Assinante" sink
e recebemos muitos artigos articles
ou erros articlesError
.Em nossa classe, NewsAPI
já construímos uma função fetchArticlesErr (from endpoint: Endpoint)
que retorna um "publicador" AnyPublisher<[Article], NewsError>
, dependendo do valor endpoint
, e só precisamos usar de alguma forma os valores dos "publicadores" $indexEndpoint
e $searchString
transformá-los em argumento para essa função endpoint
. Para começar, combinaremos os "editores" $indexEndpoint
e $searchString
. Para fazer isso, Combine
existe um operador Publishers.CombineLatest
:
Em seguida, devemos definir o tipo de erro TYPE "publisher" igual ao necessário NewsError
:
Em seguida, queremos usar a função fetchArticlesErr (from endpoint: Endpoint)
da nossa classe NewsAPI
. Como de costume, faremos isso com a ajuda de um operador flatMap
que cria um novo "editor" com base nos dados recebidos do "editor" anterior:
"Assinamos" esse "editor" recém-recebido com a ajuda de um "assinante" sink
e usamos seus fechamentos receiveCompletion
e receiveValue
para receber do "editor" o valor de uma matriz de artigos articles
ou erros articlesError
:
Naturalmente, é necessário lembrar a "assinatura" resultante em alguma init()
variável externa cancellableSet
. Caso contrário, não conseguiremos obter o valor de forma assíncronaarticles
ou um erro articlesError
após a conclusão init()
:
para reduzir o número de chamadas para o servidor ao digitar uma sequência de pesquisa searchString
, não devemos usar o "editor" da própria barra de pesquisa $searchString
, mas sua versão modificada validString
:
"Inscrevendo-se" no "editor" ASSÍNCRONO que criamos init( )
será persistir durante todo o “ciclo de vida” da instância da classe ArticlesViewModelErr
:
Procuramos a correção do nosso UI
para exibir possíveis erros de amostragem de dados nele. Em SwiftU
I, na estrutura existente, ContentVieArticles
usamos outra, apenas obtida View Model
, apenas adicionando as letras “Err” no nome. Esta é uma instância da classe. ArticlesViewModelErr
, que “captura” o erro de selecionar e / ou decodificar os dados do artigo no servidor NewsAPI.org :
Além disso, adicionamos a exibição de uma mensagem de emergência Alert
em caso de erro.Por exemplo, se a chave da API errada for:struct APIConstants {
static let apiKey: String = "API_KEY"
. . . . . . . . . . . . .
}
... então receberemos a seguinte mensagem:
Se o limite de solicitações estiver esgotado, receberemos a seguinte mensagem:
Voltando ao método de seleção de artigos [Article]
com um possível erro NewsError
, podemos simplificar seu código se usarmos o Generic
"editor" AnyPublisher<T,NewsError>,
que, com base no conjunto, url
recebe JSON
informações de forma assíncrona , as coloca diretamente no Codable
modelo T
e relata um erro NewsError
:
como sabemos, esse código é muito fácil de usar para obter um "editor" específico se os dados de origem url
forem um país ou agregador deEndpoint
notícias NewsAPI.orgcountry
fonte de informação e a saída requer vários modelos - por exemplo, uma lista de artigos ou fontes de informação:

Conclusão
Aprendemos como é fácil atender a HTTP
solicitações com a ajuda de Combine
seu URLSession
"editor" dataTaskPublisher
e Codable
. Se você não precisa rastrear erros, obtém um Generic
código de 5 linhas muito simples para o "editor" AnyPublisher<T, Never>
, que recebe JSON
informações de forma assíncrona e as coloca diretamente no Codable
modelo com T
base no dado url
:
Este código é muito fácil de usar para obter um editor específico, se os dados de origem url
forem, por exemplo Endpoint
, e a saída requer vários modelos - por exemplo, um conjunto de artigos ou uma lista de fontes de informação.Se você precisar levar em conta os erros, o código do Generic
"editor" será um pouco mais complicado, mas ainda assim será um código muito simples, sem retornos de chamada:
Usando a tecnologia de execução de HTTP
consultas Combine
, você pode criar um "editor" AnyPublisher<UIImage?, Never>
que seleciona dados de forma assíncrona e recebe uma imagem UIImage? baseado em URL
. Os ImageLoade
downloaders de imagem r são armazenados em cache na memória para evitar a recuperação repetida de dados assíncronos.Todos os tipos de "editores" obtidos podem muito facilmente ser "feitos para funcionar" nas classes ObservableObject, que usam suas propriedades @Published para controlar sua interface do usuário projetada usando o SwiftUI. Essas classes geralmente desempenham o papel do modelo de exibição, pois possuem as chamadas propriedades @Published "entrada" que correspondem aos elementos ativos da interface do usuário (caixas de texto TextField, Stepper, Picker, botões de opção Alternar etc.) e propriedades @Published "output" , consistindo principalmente em elementos passivos da interface do usuário (textos, imagens, imagens, formas geométricas em círculo (), retângulo () etc.).Essa idéia permeia todo o aplicativo agregador de notícias NewsAPI.org apresentado neste artigo. bastante universal e foi usado quandodesenvolvimento de um aplicativo para o banco de dados de filmes TMDb e o agregador de notícias Hacker News , que serão discutidos em artigos futuros.O código do aplicativo para este artigo está no Github .PS1. Quero chamar sua atenção para o fato de que, se você usar o simulador para o aplicativo apresentado neste artigo, saiba que NavigationLink
o simulador funciona com um erro. Você pode usarNavigationLink
no simulador apenas 1 vez. Essa. você usou o link, voltou, clique no mesmo link - e nada acontece. Até você usar outro link, o primeiro não funcionará, mas o segundo ficará inacessível. Mas isso é observado apenas no simulador, em um dispositivo real tudo funciona bem.2. Algumas fontes de informação ainda usam http
em vez https
de "imagens" dos seus artigos. Se você definitivamente deseja ver essas “imagens”, mas não pode controlar a fonte de sua aparência, é necessário configurar o sistema de segurança ATS ( App Transport Security)
para receber essas http
“imagens”, mas isso, obviamente, não é uma boa ideia . Você pode usar opções mais seguras .Referências:
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» )