MVVMs baseadas em combinação nos aplicativos UIKit e SwiftUI para desenvolvedores do UIKit



Sabemos que ObservableObject classes com suas @Publishedpropriedades são criadas Combineespecialmente para View Modelin SwiftUI. Mas exatamente o mesmo View Modelpode ser usado na UIKitimplementação da arquitetura  MVVM, embora neste caso tenhamos que "ligar" manualmente bindos UIelementos às @Published propriedades de View Model. Você ficará surpreso, mas com a ajuda Combinedisso, algumas linhas de código são concluídas. Além disso, aderindo a essa ideologia ao projetar UIKitaplicativos, você posteriormente mudará sem problemas para SwiftUI.

O objetivo deste artigo é mostrar com um exemplo primitivamente simples como você pode elegantemente implementar uma MVVMarquitetura UIKitcom  Combine. Por outro lado, mostramos o uso do mesmoView Model c SwiftUI.

O artigo discutirá dois aplicativos simples que permitem selecionar as informações meteorológicas mais recentes para uma cidade específica no site da OpenWeatherMa . Mas UIum deles será criado com o aplicativo SwiftUIe o outro com ajuda UIKit. Para o usuário, esses aplicativos terão a mesma aparência.



O código está no Github .

A interface do usuário ( UI) conterá apenas 2 UI elementos: um campo de texto para inserir a cidade e um rótulo para exibir a temperatura. A caixa de texto para entrar na cidade é a entrada ativa ( Input) e o rótulo de temperatura é a saída passiva ( Output).  

O papel View Model da arquitetura MVVMé que leva a entrada (s) da View(ou ViewControllerem UIKit), implementa a lógica de negócios do aplicativo e passa as saídas de volta para  View(ou ViewControllerem UIKit), possivelmente apresentando esses dados no formato desejado.

Criando  View Modelcom Combinenão importa que tipo de lógica de negócios - síncrona ou assíncrona - é muito simples se você usar uma ObservableObject classe com suas @Publishedpropriedades.

API OpenWeatherMap


Embora o serviço  OpenWeatherMap   permita a seleção de informações meteorológicas muito extensas, o Modelo dos dados em que estamos interessados ​​será muito simples, fornece informações detalhadas  WeatherDetailsobre o clima atual na cidade selecionada e está localizado no arquivo  Model.swift :



Embora nesta tarefa específica estaremos interessados ​​apenas em temperatura temp, que está na estrutura  Main, o Modelo fornece informações detalhadas completas sobre o clima atual como uma estrutura raiz  WeatherDetail, acreditando que no futuro você desejará expandir os recursos deste aplicativo. A estrutura WeatherDetail é codificável, isso nos permitirá decodificar literalmente os JSONdados no modelo com apenas duas linhas de código  .

A estrutura  WeatherDetail também deve serIdentifiablese desejarmos tornar mais fácil para nós mesmos, no futuro, exibir uma série de previsões do tempo  [WeatherDetail] por vários dias antes, na forma de uma lista  List de SwiftUI. Este também é um espaço em branco para um futuro aplicativo meteorológico atual mais sofisticado. O protocolo Identifiablerequer a presença da propriedade id,que já possuímos, portanto nenhum esforço adicional será necessário de nós.

Geralmente, os serviços, incluindo o serviço  OpenWeatherMap , oferecem todos os tipos de serviços URLs para obter os recursos de que precisamos. O serviço  OpenWeatherMap oferece-nos URLspara obter informações detalhadas sobre o tempo atual ou previsão de 5 dias em uma determinada cidade city. Nesta aplicação, estaremos interessados ​​apenas nas informações meteorológicas atuais e, neste caso,URLcalculado usando a função absoluteURL (city: String):



API para o serviço OpenWeatherMap ,  o colocaremos no arquivo WeatherAPI.swift . Sua parte central será um método para selecionar informações meteorológicas detalhadas  WeatherDetailem uma cidade  city:

  • fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>

No contexto da estrutura, Combine esse método retorna não apenas informações meteorológicas detalhadas  WeatherDetail, mas o "editor" correspondente Publisher. Nosso "editor" AnyPublisher<WeatherDetail, Never>não retorna nenhum erro - Nevere se ainda ocorreu um erro de amostragem ou codificação, o representante retorna  WeatherDetail.placeholdersem nenhuma mensagem adicional sobre a causa do erro. 

Considere com mais detalhes o método  fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>que seleciona  informações meteorológicas detalhadas para a cidade no site do OpenWeatherMap city e não retorna nenhum erro Never:



  1. com base no nome da cidade,  city formamos URL usando a função absoluteURL(city:city)para solicitar informações detalhadas do tempo  WeatherDetail,
  2. «» dataTaskPublisher(for:), Output (data: Data, response: URLResponse),   Failure - URLError,
  3. map { } (data: Data, response: URLResponse)  data
  4. JSON  data ,  WeatherDetail, ,
  5. - «»  catch (error ... )  «» WeatherDetail.placeholder,
  6. main , UI,
  7. «» «» eraseToAnyPublisher() AnyPublisher.

O "publicador" assíncrono assim obtido AnyPublisher"não decola" por si só; ele não entrega nada até que alguém "assine". Vamos usá-lo em uma  ObservableObject classe que desempenha um papel View Modelem  SwiftUIambos e UIKit

Criar modelo de vista


Para View Modelcriar uma classe muito simples  TempViewModelque implementa um protocolo ObservableObject com duas  @Published propriedades:  



  1. uma  @Published var city: Stringé a cidade (você pode chamá-la condicionalmente de ENTRADA, já que seu valor é regulado pelo usuário View),  
  2. o segundo  @Published var currentWeather = WeatherDetail.placeholder é o clima nesta cidade no momento (podemos chamar condicionalmente essa propriedade de EXIT, pois ela é obtida buscando dados no site do  OpenWeatherMap ).

Depois de definir uma  @Published propriedade  city, podemos começar a usá-la como uma propriedade simples  citye como uma "editora"  $city.

Em uma classe  TempViewModel, 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 fim, ao inicializar uma instância de classe  TempViewModel em, init?podemos criar uma "assinatura" que funcionará durante todo o "ciclo de vida" da instância de classe  TempViewModele reproduzirá a dependência do clima atual  currentWeather na cidade  city.

Para fazer isso, Combineestendemos a cadeia do "editor" de entrada para o "editor" $city de saída AnyPublisher<WeatherDetail, Never>, cujo valor é o clima atual. Posteriormente, nós "assinamos" com a ajuda de um "assinante" assign (to: \.currentWeather, on: self) e obtenha o valor desejado do clima atual  currentWeather como uma @Published propriedade de "saída"  .

Nós devemos puxar a cadeia NÃO simplesmente das propriedades city, ou seja, dos "editores" $cityque participarão da criação UI e é aí que a mudaremos.

Como faremos isso?

Já temos uma função em nosso arsenal fetchWeather (for city: String)que está na classe  WeatherAPIe retorna o “publicador” AnyPublisher<WeatherDetail, Never> com informações detalhadas do tempo, dependendo da cidade  city, e só podemos de alguma forma usar o valor do “publicador”  $citypara transformá-lo em argumento dessa função.

 Vá para o editor certo  fetchWeather (for city: String) para  Combinenos ajudar na operadora  flatMap:



OperadoraflatMapcria um novo "editor" com base nos dados recebidos do "editor" anterior.

Em seguida, nós "assinamos" este "editor" recém-recebido com a ajuda de um "assinante" muito simples  assign (to: \.currentWeather, on: self)e atribuímos o valor recebido do "editor" à @Publishedpropriedade  currentWeather:



Acabamos de criar um init( )"editor" ASSÍNCRONO e um "assinante", resultando em uma AnyCancellable"assinatura" "

AnyCancellable A "assinatura" permite que o chamador cancele a "assinatura" a qualquer momento e não receba mais valores do "editor", mas além disso, assim que a  AnyCancellable"assinatura" sair do seu escopo, a memória ocupada pelo "editor" é liberada. Portanto, assim que for init( ) concluída, essa "assinatura" será excluída pelo sistema ARCe sem tempo para atribuir as informações assíncronas sobre o clima atual recebido com um atraso de tempo  currentWeather. Para salvar essa “assinatura”, é necessário criar uma init()variável OUTSIDE var cancellableSetque manterá nossa AnyCancellable“assinatura” nessa variável durante todo o “ciclo de vida” da instância da classe  TempViewMode

A AnyCancellable"assinatura" na variável é armazenada cancellableSetusando o operador  store ( in: &self.cancellableSet):



Como resultado, a "assinatura" será preservada durante todo o "ciclo de vida" da instância da classe  TempViewModel. Podemos alterar o valor do editor conforme desejado $city, e o clima atual currentWeather para esta cidade estará sempre à nossa disposição .

Para reduzir o número de chamadas do servidor ao digitar uma cidade city, não devemos usar diretamente o "publicador" da linha com o nome da cidade  $city, mas sua versão modificada com operadores debouncee removeDuplicates:



O operador é  debounce usado para aguardar até que o usuário termine de digitar as informações necessárias no teclado e só então execute a tarefa com muitos recursos uma vez.

Da mesma forma, um operador removeDuplicatespublicará valores somente se eles diferirem de quaisquer valores anteriores. Por exemplo, se o usuário entrar pela primeira johnvez joe, e depois novamente john, receberemos johnapenas uma vez. Isso ajuda a tornar a nossa UImais eficiente.

Criando uma interface do usuário com o SwiftUI


Agora que temos View Model, vamos começar UI. Primeiro em SwiftUIe depois em UIKit.

Ao Xcodecriarmos um novo projeto com SwiftUIe na estrutura resultante, ContentView  colocamos o nosso  View Modelcomo uma @ObservedObject variável model. Substitua  Text ("Hello, World!") pelo título  Text ("WeatherApp"), adicione uma caixa de texto para inserir a cidade  TextField ("City", text: self.$model.city) e um rótulo para exibir a temperatura:



Usamos diretamente os valores de nossa variável model: TempViewModel(). Usamos na caixa de texto para entrar na cidade $model.citye no rótulo para exibir a temperatura - model.currentWeather.main?.temp.

Agora, quaisquer alterações nas  @Published propriedades levarão a "redesenho" View:



Isso é garantido pelo fato de que a nossa View Model é@ObservedObject, ou seja, é realizada a "ligação" AUTOMÁTICA ( binding) @Publishedde nossas propriedades View Modele elementos da interface do usuário ( UI). Essa "ligação" AUTOMÁTICA só é possível em SwiftUI.

Criando uma interface do usuário com o UIKit


O que fazer com isso UIKit? Não está lá  @ObservedObject. Em  UIKit executaremos a "ligação" ( binding) manualmente. Há muitas maneiras de fazer essa "ligação manual":

  • Key-Value Observing ou KVO: um mecanismo para  key pathsmonitorar uma propriedade e receber uma notificação de que ela foi alterada.
  • Programação reativa funcional ou FRP: uso de uma estrutura Combine.
  • Delegation: Usando métodos delegados para enviar uma notificação de que um valor da propriedade foi alterado.
  • Boxing: didSet { } , .

Dado o título do artigo, trabalharemos naturalmente no campo Combine. No UIKitaplicativo, mostraremos como é fácil criar "encadernação manual" Combine.

No UIKitaplicativo, também teremos dois UI elementos: UITextFieldpara entrar na cidade e UILabelexibir a temperatura. Em ViewControllernós, naturalmente, terá Outletestes elementos:





Sob a forma de uma variável comum viewModel, temos o mesmo que View Modelna seção anterior:



Antes de fazer o “Manual de ligação” com Combine, vamos fazer o campo de texto UITextFieldnosso aliado e “publisher” do nosso conteúdo text:



Isso nos permitirá viewDidLoadimplementar muito facilmente “encadernação manual” usando a funçãobinding ():



Na verdade, nós “subscribe” para o “publisher” cityTextField.textPublisherusando um “assinante” muito simples  assign (to: \.city, on: viewModel)e atribuir o texto digitado pelo usuário na caixa de texto cityTextFieldpara o nosso “input”  @Publishedde propriedade de citynosso View Model.

Além disso, fazemos alterações em outra direção: “assinamos” a @Publishedpropriedade  “output”  $currentWeather com a ajuda do “assinante” sink e seu fechamento receiveValue, formamos o valor da temperatura e atribuímos à etiqueta temperatureLabel.

Recebido na  viewDidLoad "assinatura" é armazenado em uma variável var cancellableSet. Depois de criá-los uma vez, permitimos que eles ajam durante todo o “ciclo de vida” da instância da classe ViewControllere em conjunto com a “assinatura” em nossa View Modelimplementar toda a lógica de negócios do aplicativo.

A propósito, o protocolo ObservableObjectnão funciona UIKit, mas não interfere. UIKit completamente indiferente ao protocolo ObservableObjecte, em princípio, poderia ser removido  View Modelas UIKit aplicações:



Mas não vamos fazer isso, já que queremos mantê-lo inalterado View Modelpara o aplicativo atual na UIKite para, possivelmente, futuras aplicações em SwiftUI.

Isso é tudo. O código está no Github .

Conclusão

Uma estrutura reativa funcional Combinepermite implementar de maneira muito simples e concisa a MVVMarquitetura SwiftUIna forma UIKite na forma de código compreensível e legível.

Links:

Combinar + UIKit + MVVM

Tutorial do Combine iOS MVVM: Refatorando do MVC
MVVM com o Tutorial do Combine para iOS

PS Se você quiser ver algumas informações sobre o clima, é necessário se registrar no OpenWeatherMap  e obter API key. Esse processo não levará mais de 2 minutos.

All Articles