A consulta HTTPé uma das habilidades mais importantes que você precisa obter ao desenvolver iOSaplicativos. 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 5nova estrutura da programação reativa funcional Combineem conjunto com a existente URLSessione Codablefornece 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, Combinecriaremos "editores"Publisherpara selecionar dados da Internet, nos quais podemos "facilmente assinar" no futuro e usar ao projetar UIcom UIKite com ajuda SwiftUI.Como SwiftUIparece mais conciso e mais eficaz, porque a ação dos "editores" Publishernão se limita apenas aos dados de amostra e se estende até o controle da interface do usuário ( UI). O fato é que SwiftUIa separação de dados é View realizada usando ObservableObjectclasses com @Publishedpropriedades, cujas alterações são SwiftUImonitoradas AUTOMATICAMENTE e redesenhadas completamente View.Nessas ObservableObjectclasses, 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, Toggleetc.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 Endpointque 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 Endpointna 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 ObservableObjectclasses 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-keyou 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 SwiftUIserá usado no mínimo, sem frescuras e apenas para mostrar como, Combinecom seus "editores"Publishere "assinatura" são Subscriptionafetados UI.É recomendável que você se registre no site NewsAPI.org e receba a chave APInecessá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 Articleconterá o identificador id, título title, descrição description, autor author, URL da “imagem” urlToImage, data da publicação publishedAte fonte da publicação source. Acima dos artigos, [Article]há um complemento NewsResponseno qual estaremos interessados apenas na propriedade articles, que é uma variedade de artigos. A estrutura raiz NewsResponsee a estrutura Articlesão Codable, o que nos permitirá literalmente duas linhas de código decodificando os JSONdados 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 propriedadeidque forneceremos com um identificador exclusivo artificial UUID().A fonte de informações Sourceconterá 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 SourcesResponseno qual estaremos interessados apenas em uma propriedade sources, que é uma matriz de fontes de informação. A estrutura SourcesResponsee a estrutura raiz Sourcesão Codable, o que nos permitirá decodificar com muita facilidade os JSONdados 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 Identifiablerequer a presença da propriedade idque já possuímos, portanto nenhum esforço adicional será necessário de nós.Agora considere o que precisamos APIpara o serviço NewsAPI.org e coloque-o no arquivo NewsAPI.swift . A parte central da nossa API é uma classe NewsAPIque 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 - Nevere 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 Endpointinicializador 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 endpointe 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 URLpara 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 Genericfunçã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" Publishercomo 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 Viewsã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 ObservableObjectpode ser transformado em uma única “fonte de verdade” externa (fonte de verdade). Para "publishers" ordinários tipo Timerou NotificationCenter Viewreage usando o método onReceive (_: perform:). Um exemplo do uso do "editor" Timerque 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 Endpointusuário escolher, precisamos atualizar a lista de artigos articlesselecionados no NewsAPI.org . Para fazer isso, criaremos uma classe muito simples ArticlesViewModelque implementa um protocolo ObservableObject com três @Publishedpropriedades: 
@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" $indexEndpointe $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 , initpodemos criar uma "assinatura" que atuará durante todo o "ciclo de vida" da instância da classe ArticlesViewModele reproduzirá a dependência da lista de artigos articlesno índice indexEndpoint e na cadeia de pesquisa searchString.Para fazer isso, Combineestendemos 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 indexEndpointe searchString, principalmente dos "editores" $indexEndpointe $searchStringque participam da criação UIcom a ajuda de, SwiftUIe nós os alteraremos lá usando os elementos da interface do usuário Pickere TextField.Como faremos isso?Já temos uma função em nosso arsenal fetchArticles (from: Endpoint)que está na classe NewsAPIe retorna um "editor" AnyPublisher<[Article], Never>, dependendo do valorEndpoint, e só podemos usar de alguma forma os valores dos "editores" $indexEndpointe $searchStringtransformá-los em um argumento para endpointessa função. Primeiro, combine os "editores" $indexEndpoint e $searchString. Para fazer isso, o Combineoperador 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 cancellableSetque salvará nossa AnyCancellable"assinatura" nessa variável durante todo o "ciclo de vida" da instância da classe ArticlesViewMode. Portanto, removemos a constante let subscriptione lembramos da nossa AnyCancellable"assinatura" na variável cancellableSetusando 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" $indexEndpointe / ou searchString, e sempre graças à "assinatura" criada, teremos uma variedade de artigos correspondentes aos valores desses dois editores articlessem nenhum esforço adicional. Essa ObservableObjectclasse 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 $searchStringe sua versão modificada validString:
agora que temos View Modelnossos artigos, vamos começar a criar a interface do usuário ( UI). Para SwiftUIsincronizar Viewcom 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 @ObservedObjectvariá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 articleViewModele a substituímos Text ("Hello, World!")por uma lista de artigos ArticlesListna qual colocamos os artigos articlesViewModel.articlesobtidos 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 UIelemento à nossa tela para controlar qual conjunto de artigos queremos exibir. Usaremos a Pickeralteraçã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 " @Publishededitor" 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 SearchViewpara 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 ObservableObjectModelo extremamente simples que possua apenas duas @Publishedpropriedades 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 initcriamos uma “assinatura” que funcionará durante todo o “ciclo de vida” da instância da classe SourcesViewModele garantimos que a lista de fontes de informações dependa sourcesdo país countrye 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 cancellableSetusando o operador .store ( in: &self.cancellableSet).Agora que temos View Modelnossas fontes de informação, vamos começar a criar UI. B SwiftUIpara sincronizar ViewcObservableObject O modelo usa uma @ObservedObjectvariável que se refere a uma instância da classe deste modelo.Adicione a ContentViewSources instância da classe à estrutura SourcesViewModelna forma de uma variável var sourcesViewModel, remova Text ("Hello, World!") e coloque a sua Viewpara cada uma das 3 @Publishedpropriedades sourcesViewModel : - caixa de texto
SearchViewpara 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 SearchViewe o “país” com Picker, e o resto acontece automaticamente.A lista de fontes de informação SourcesListconté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 NavigationLinkno qual destinationindicamos DetailSourceViewquais dados de fonte são a fonte de informação sourcee 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 ArticlesViewModelpara a qual devemos definir as duas @Publishedpropriedades 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 UIImagepara o artigo Article.
O modelo do artigo Article contém uma URLimagem urlToImageque o acompanha : com
base nisso, URLno 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
urligual nil, retorne Just(nil), - com base na
urlforma 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 datae formulário adicionais UIImage, - se ocorrer o erro de retorno das etapas anteriores
nil, - entregamos o resultado ao
mainfluxo, 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 ImageLoaderque implementa o protocolo ObservableObject, com duas @Publishedpropriedades:@Published url: URL? são URLimagens@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 flatMape 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 AnyCancellableusando o operador store(in: &self.cancellableSet).A lógica desse “downloader de imagens” é que você baixa uma imagem de algo diferente daquele, nil URLdesde 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, imagepermanecerá igual nil.Em SwiftUImostramos a imagem com a ajuda ArticleImageque 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 Rectanglecom 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 nilobter 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 URLimagem, mas o acesso é fechado e obtemos um retângulo Rectanglecom texto rotativo Text("Loading...")que nunca será substituído:
Nesta situação, se a URLimagem for diferente nil, a igualdade da nilimagem imagepode 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 @Publishedpropriedades 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", initcapturamos todos os erros Errorque ocorrem ao carregar a imagem e acumulamos sua presença na @Publishedpropriedade 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 fetchImageErrinicializando o "publicador" Future, que pode ser usado para obter de forma assíncrona um único valor TYPE Resultusando um fechamento. O fechamento possui um parâmetro - Promiseque é uma função do TYPE (Result<Output, Failure>) → Void: Transformaremos o
resultado FutureemAnyPublisher <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 urlde nil e noDataem true: em caso afirmativo, em seguida, retornar o erro, se não, a transferência de urlmais longe, cadeia,1. crie um "publicador" dataTaskPublisher(for:)cuja entrada seja - urle o valor de saída Outputseja uma tupla (data: Data, response: URLResponse)e um erro URLError,2. analise usando a tryMap { } tupla resultante (data: Data, response: URLResponse): se response.statusCodeestiver 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 datapara UIImage,4. entregar o resultado para o mainfluxo, uma vez que assumimos que vamos usá-lo mais tarde no projeto UI- que “assinar” para o “publisher” recebeu usando sinkseus fechamentos receiveCompletione 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 cancellableSetpara 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 ArticleImageonde usaremos a nova @Publishedvariá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-keyou, 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 HTTPcó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 Resultusando um fechamento. O fechamento possui um parâmetro - Promiseque é uma função do TYPE (Result<Output, Failure>) -> Void: Transformaremos os
recebidos Futureno "editor" necessário, AnyPublisher <[Article], NewsError>usando o operador "TYPE Erase" eraseToAnyPublisher().Mais adiante no novo método, fetchArticlesErrrepetiremos 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 URLErrorse ocorrer um erro no lado do cliente (incapacidade de entrar em contato com o servidor, proibição do sistema de segurança ATSetc.).Se queremos exibir erros SwiftUI, precisamos do correspondente View Model, que chamaremos ArticlesViewModelErr:
Na classe ArticlesViewModelErrque implementa o protocolo ObservableObject , desta vez temos QUATRO @Publishedpropriedades:@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" $indexEndpointe $searchStringsaída "publisher" AnyPublisher<[Article],NewsError>, à qual "assinamos" com o "Assinante" sinke recebemos muitos artigos articlesou erros articlesError.Em nossa classe, NewsAPIjá 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" $indexEndpointe $searchStringtransformá-los em argumento para essa função endpoint. Para começar, combinaremos os "editores" $indexEndpoint e $searchString. Para fazer isso, Combineexiste 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 flatMapque cria um novo "editor" com base nos dados recebidos do "editor" anterior:
"Assinamos" esse "editor" recém-recebido com a ajuda de um "assinante" sinke usamos seus fechamentos receiveCompletione receiveValuepara receber do "editor" o valor de uma matriz de artigos articlesou 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íncronaarticlesou 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 UIpara exibir possíveis erros de amostragem de dados nele. Em SwiftUI, 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, urlrecebe JSONinformações de forma assíncrona , as coloca diretamente no Codablemodelo 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 urlforem 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 HTTPsolicitações com a ajuda de Combineseu URLSession"editor" dataTaskPublishere Codable. Se você não precisa rastrear erros, obtém um Genericcó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 HTTPconsultas 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 ImageLoadedownloaders 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 NavigationLinko simulador funciona com um erro. Você pode usarNavigationLinkno 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 httpem vez httpsde "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» )