Tutorial Swift: Uma introdução ao padrão de design MVVM

Publicados: 2022-03-11

Então, você está iniciando um novo projeto iOS, recebeu do designer todos os documentos .pdf e .sketch necessários e já tem uma visão de como construirá esse novo aplicativo.

Você começa a transferir telas de UI dos esboços do designer para seus ViewController .swift , .xib e .storyboard do ViewController.

UITextField aqui, UITableView ali, mais alguns UILabels e uma pitada de UIButtons . IBOutlets e IBActions também estão incluídos. Tudo bem, ainda estamos na zona de interface do usuário.

No entanto, é hora de fazer algo com todos esses elementos de interface do usuário; UIButtons receberão toques de dedo, UILabels e UITableViews precisarão de alguém para dizer a eles o que exibir e em qual formato.

De repente, você tem mais de 3.000 linhas de código.

3.000 linhas de código Swift

Você acabou com um monte de código de espaguete.

A primeira etapa para resolver isso é aplicar o padrão de design Model-View-Controller (MVC). No entanto, esse padrão tem seus próprios problemas. Aí vem o padrão de design Model-View-ViewModel (MVVM) que salva o dia.

Lidando com o código de espaguete

Em pouco tempo, seu ViewController inicial se tornou muito inteligente e muito grande.

Código de rede, código de análise de dados, código de ajustes de dados para a apresentação da interface do usuário, notificações de estado do aplicativo, alterações de estado da interface do usuário. Todo aquele código aprisionado dentro do if -ology de um único arquivo que não pode ser reutilizado e caberia apenas neste projeto.

Seu código ViewController se tornou o infame código de espaguete.

Como isso aconteceu?

O provável motivo é algo assim:

Você estava com pressa para ver como os dados de back-end estavam se comportando dentro do UITableView , então você colocou algumas linhas de código de rede dentro de um método temporário do ViewController apenas para buscar aquele .json da rede. Em seguida, você precisava processar os dados dentro desse .json , então você escreveu outro método temporário para fazer isso. Ou, pior ainda, você fez isso dentro do mesmo método.

O ViewController continuou crescendo quando o código de autorização do usuário apareceu. Em seguida, os formatos de dados começaram a mudar, a interface do usuário evoluiu e precisou de algumas mudanças radicais, e você continuou adicionando mais if s em uma já enorme if -ology.

Mas, como é que o UIViewController é o que saiu do controle?

O UIViewController é o local lógico para começar a trabalhar em seu código de interface do usuário. Ele representa a tela física que você está vendo ao usar qualquer aplicativo com seu dispositivo iOS. Até a Apple usa UIViewControllers em seu aplicativo principal do sistema quando alterna entre diferentes aplicativos e suas UIs animadas.

A Apple baseia sua abstração de interface do usuário dentro do UIViewController , pois está no núcleo do código da interface do usuário do iOS e faz parte do padrão de design MVC .

Relacionado: Os 10 erros mais comuns que os desenvolvedores iOS não sabem que estão cometendo

Atualizando para o padrão de design MVC

Padrão de design MVC

No padrão de design MVC, View deve estar inativo e exibe apenas dados preparados sob demanda.

O controlador deve trabalhar nos dados do modelo para prepará-lo para as exibições , que exibem esses dados.

A View também é responsável por notificar o Controlador sobre quaisquer ações, como toques do usuário.

Como mencionado, UIViewController geralmente é o ponto de partida na construção de uma tela de interface do usuário. Observe que em seu nome, ele contém a “view” e o “controller”. Isso significa que ele “controla a visualização”. Isso não significa que tanto o código do “controlador” quanto o da “visualização” devam entrar.

Essa mistura de código de exibição e controlador geralmente acontece quando você move IBOutlets de pequenas subvisualizações dentro do UIViewController e manipula essas subvisualizações diretamente do UIViewController . Em vez disso, você deveria ter encapsulado esse código dentro de uma subclasse UIView personalizada.

É fácil ver que isso pode fazer com que os caminhos do código View e Controller sejam cruzados.

MVVM ao resgate

É aqui que o padrão MVVM é útil.

Como o UIViewController deve ser um Controller no padrão MVC, e já está fazendo muito com as Views , podemos mesclá-los na View do nosso novo padrão - MVVM .

Padrão de design MVVM

No padrão de design MVVM, Model é o mesmo que no padrão MVC. Representa dados simples.

A visualização é representada pelos objetos UIView ou UIViewController , acompanhados de seus arquivos .xib e .storyboard , que devem exibir apenas dados preparados. (Não queremos ter o código NSDateFormatter , por exemplo, dentro da View.)

Apenas uma string simples e formatada que vem do ViewModel .

O ViewModel oculta todo o código de rede assíncrono, código de preparação de dados para apresentação visual e escuta de código para alterações de modelo . Tudo isso está oculto por trás de uma API bem definida, modelada para se adequar a essa visualização específica.

Um dos benefícios de usar o MVVM é testar. Como ViewModel é NSObject puro (ou struct , por exemplo), e não é acoplado ao código UIKit , você pode testá-lo mais facilmente em seus testes de unidade sem afetar o código da interface do usuário.

Agora, o View ( UIViewController / UIView ) ficou muito mais simples enquanto o ViewModel atua como a cola entre o Model e o View .

Aplicando MVVM no Swift

MVVM em Swift

Para mostrar o MVVM em ação, você pode baixar e examinar o exemplo de projeto Xcode criado para este tutorial aqui. Este projeto usa Swift 3 e Xcode 8.1.

Existem duas versões do projeto: Starter e Finished.

A versão Finished é uma mini aplicação completa, onde Starter é o mesmo projeto mas sem os métodos e objetos implementados.

Primeiro, sugiro que você baixe o projeto Starter e siga este tutorial. Se você precisar de uma referência rápida do projeto para mais tarde, baixe o Projeto Concluído .

Introdução ao projeto tutorial

O projeto tutorial é um aplicativo de basquete para o rastreamento das ações dos jogadores durante o jogo.

Aplicativo de basquete

É usado para o rastreamento rápido dos movimentos do usuário e da pontuação geral em um jogo de coleta.

Duas equipes jogam até que a pontuação de 15 (com pelo menos uma diferença de dois pontos) seja alcançada. Cada jogador pode marcar um ponto a dois pontos, e cada jogador pode dar assistência, rebote e falta.

A hierarquia do projeto se parece com isso:

Hierarquia do projeto

Modelo

  • Game.swift
    • Contém a lógica do jogo, rastreia a pontuação geral, rastreia os movimentos de cada jogador.
  • Team.swift
    • Contém nome da equipe e lista de jogadores (três jogadores em cada equipe).
  • Player.swift
    • Um único jogador com um nome.

Visualizar

  • HomeViewController.swift
    • Controlador de exibição raiz, que apresenta o GameScoreboardEditorViewController
  • GameScoreboardEditorViewController.swift
    • Complementado com a visualização Interface Builder em Main.storyboard .
    • Tela de interesse para este tutorial.
  • PlayerScoreboardMoveEditorView.swift
    • Complementado com a visualização Interface Builder em PlayerScoreboardMoveEditorView.xib
    • A subvisão da visualização acima também usa o padrão de design MVVM.

ViewModel

  • O grupo ViewModel está vazio, é isso que você construirá neste tutorial.

O projeto Xcode baixado já contém espaços reservados para os objetos View ( UIView e UIViewController ). O projeto também contém alguns objetos customizados feitos para demonstrar uma das formas de como fornecer dados aos objetos ViewModel (grupo Services ).

O grupo Extensions contém extensões úteis para o código da interface do usuário que não estão no escopo deste tutorial e são autoexplicativas.

Se você executar o aplicativo neste momento, ele mostrará a interface do usuário finalizada, mas nada acontece quando um usuário pressiona os botões.

Isso ocorre porque você criou apenas visualizações e IBActions sem conectá-los à lógica do aplicativo e sem preencher os elementos da interface do usuário com os dados do modelo (do objeto Game , como aprenderemos mais adiante).

Conectando View e Model com ViewModel

No padrão de design MVVM, View não deve saber nada sobre o Model. A única coisa que View sabe é como trabalhar com um ViewModel.

Comece examinando sua View.

No arquivo GameScoreboardEditorViewController.swift , o método fillUI está vazio neste ponto. Este é o local em que você deseja preencher a interface do usuário com dados. Para conseguir isso, você precisa fornecer dados para o ViewController . Você faz isso com um objeto ViewModel.

Primeiro, crie um objeto ViewModel que contenha todos os dados necessários para este ViewController .

Vá para o grupo de projetos ViewModel Xcode, que estará vazio, crie um arquivo GameScoreboardEditorViewModel.swift e torne-o um protocolo.

 import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

Usar protocolos como esse mantém as coisas limpas e bonitas; você só deve definir os dados que usará.

Em seguida, crie uma implementação para este protocolo.

Crie um novo arquivo, chamado GameScoreboardEditorViewModelFromGame.swift , e torne esse objeto uma subclasse de NSObject .

Além disso, faça-o em conformidade com o protocolo GameScoreboardEditorViewModel :

 import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // "/ 100" <- because we want only 1 digit let totalSeconds: Int = totalMillis / 1000 let seconds: Int = totalSeconds % 60 let minutes: Int = (totalSeconds / 60) return String(format: "%02d:%02d.%d", minutes, seconds, millis) } fileprivate static func timeRemainingPretty(for game: Game) -> String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: "\(game.homeTeamScore) - \(game.awayTeamScore)") } }

Observe que você forneceu tudo o que é necessário para o ViewModel funcionar por meio do inicializador.

Você forneceu o objeto Game , que é o Model abaixo deste ViewModel.

Se você executar o aplicativo agora, ele ainda não funcionará porque você não conectou esses dados do ViewModel ao próprio View.

Então, volte para o arquivo GameScoreboardEditorViewController.swift e crie uma propriedade pública chamada viewModel .

Faça do tipo GameScoreboardEditorViewModel .

Coloque-o logo antes do método viewDidLoad dentro do GameScoreboardEditorViewController.swift .

 var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

Em seguida, você precisa implementar o método fillUI .

Observe como esse método é chamado de dois lugares, o observador da propriedade viewModel ( didSet ) e o método viewDidLoad . Isso ocorre porque podemos criar um ViewController e atribuir um ViewModel a ele antes de anexá-lo a uma visualização (antes que o método viewDidLoad seja chamado).

Por outro lado, você pode anexar a view do ViewController a outra view e chamar viewDidLoad , mas se viewModel não estiver definido naquele momento, nada acontecerá.

É por isso que primeiro você precisa verificar se tudo está definido para que seus dados preencham a interface do usuário. É importante proteger seu código contra uso inesperado.

Então, vá para o método fillUI e substitua-o pelo seguinte código:

 fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? "Start" : "Pause" self.pauseButton.setTitle(title, for: .normal) }

Agora, implemente o método pauseButtonPress :

 @IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

Tudo o que você precisa fazer agora é definir a propriedade viewModel real neste ViewController . Você faz isso “de fora”.

Abra o arquivo HomeViewController.swift e descomente o ViewModel; crie e configure linhas no método showGameScoreboardEditorViewController :

 // uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

Agora, execute o aplicativo. Deve ser algo assim:

Aplicativo iOS

A visualização do meio, que é responsável pela pontuação, tempo e nomes da equipe, não mostra mais os valores definidos no Interface Builder.

Agora, está mostrando os valores do próprio objeto ViewModel, que obtém seus dados do objeto Model real (objeto Game ).

Excelente! Mas e as visualizações dos jogadores? Esses botões ainda não fazem nada.

Você sabe que tem seis visualizações para rastreamento de movimentos do jogador.

Você criou uma subvisualização separada, chamada PlayerScoreboardMoveEditorView para isso, que não faz nada com os dados reais por enquanto e exibe valores estáticos que foram definidos por meio do Interface Builder dentro do arquivo PlayerScoreboardMoveEditorView.xib .

Você precisa fornecer alguns dados.

Você fará isso da mesma maneira que fez com GameScoreboardEditorViewController e GameScoreboardEditorViewModel .

Abra o grupo ViewModel no projeto Xcode e defina o novo protocolo aqui.

Crie um novo arquivo chamado PlayerScoreboardMoveEditorViewModel.swift e coloque o seguinte código dentro:

 import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Este protocolo ViewModel foi projetado para caber em seu PlayerScoreboardMoveEditorView , assim como você fez na visualização pai, GameScoreboardEditorViewController .

Você precisa ter valores para os cinco movimentos diferentes que um usuário pode fazer e precisa reagir quando o usuário tocar em um dos botões de ação. Você também precisa de uma String para o nome do jogador.

Depois de fazer isso, crie uma classe concreta que implemente esse protocolo, assim como você fez com a visualização pai ( GameScoreboardEditorViewController ).

Em seguida, crie uma implementação deste protocolo: Crie um novo arquivo, nomeie-o PlayerScoreboardMoveEditorViewModelFromPlayer.swift e torne este objeto uma subclasse de NSObject . Além disso, faça-o em conformidade com o protocolo PlayerScoreboardMoveEditorViewModel :

 import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = "\(game.playerMoveCount(for: player, move: .onePoint))" self.twoPointMoveCount = "\(game.playerMoveCount(for: player, move: .twoPoints))" self.assistMoveCount = "\(game.playerMoveCount(for: player, move: .assist))" self.reboundMoveCount = "\(game.playerMoveCount(for: player, move: .rebound))" self.foulMoveCount = "\(game.playerMoveCount(for: player, move: .foul))" } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = "\(game.playerMoveCount(for: player, move: .onePoint))" twoPointMoveCount = "\(game.playerMoveCount(for: player, move: .twoPoints))" assistMoveCount = "\(game.playerMoveCount(for: player, move: .assist))" reboundMoveCount = "\(game.playerMoveCount(for: player, move: .rebound))" foulMoveCount = "\(game.playerMoveCount(for: player, move: .foul))" } }

Agora, você precisa ter um objeto que criará essa instância “de fora” e defini-la como uma propriedade dentro do PlayerScoreboardMoveEditorView .

Lembra como HomeViewController foi responsável por definir a propriedade viewModel no GameScoreboardEditorViewController ?

Da mesma forma, GameScoreboardEditorViewController é uma view pai do seu PlayerScoreboardMoveEditorView e esse GameScoreboardEditorViewController será responsável pela criação dos objetos PlayerScoreboardMoveEditorViewModel .

Você precisa expandir seu GameScoreboardEditorViewModel primeiro.

Abra GameScoreboardEditorViewMode l e adicione estas duas propriedades:

 var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Além disso, atualize o GameScoreboardEditorViewModelFromGame com essas duas propriedades logo acima do método initWithGame :

 let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Adicione estas duas linhas dentro initWithGame :

 self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

E, claro, adicione o método playerViewModelsWithPlayers ausente:

 // MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

Excelente!

Você atualizou seu ViewModel ( GameScoreboardEditorViewModel ) com a matriz de jogadores em casa e fora. Você ainda precisa preencher esses dois arrays.

Você fará isso no mesmo local em que usou este viewModel para preencher a interface do usuário.

Abra GameScoreboardEditorViewController e vá para o método fillUI . Adicione estas linhas no final do método:

 homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

No momento, você tem erros de compilação porque não adicionou a propriedade viewModel real dentro do PlayerScoreboardMoveEditorView .

Adicione o seguinte código acima do init method inside the PlayerScoreboardMoveEditorView`.

 var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

E implemente o método fillUI :

 fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

Por fim, execute o aplicativo e veja como os dados nos elementos da interface do usuário são os dados reais do objeto Game .

Aplicativo iOS

Neste ponto, você tem um aplicativo funcional que usa o padrão de design MVVM.

Ele oculta bem o modelo da visualização, e sua visualização é muito mais simples do que você está acostumado com o MVC.

Até este ponto, você criou um aplicativo que contém o View e seu ViewModel.

Essa View também tem seis instâncias da mesma subview (view player) com seu ViewModel.

No entanto, como você pode notar, você só pode exibir dados na interface do usuário uma vez (no método fillUI ) e esses dados são estáticos.

Se seus dados nas exibições não forem alterados durante a vida útil dessa exibição, você terá uma solução boa e limpa para usar o MVVM dessa maneira.

Tornando o ViewModel dinâmico

Como seus dados serão alterados, você precisa tornar seu ViewModel dinâmico.

O que isso significa é que quando o Model é alterado, ViewModel deve alterar seus valores de propriedade pública; ele propagaria a alteração de volta para a exibição, que é a que atualizará a interface do usuário.

Há muitas maneiras de fazer isso.

Quando o modelo muda, o ViewModel é notificado primeiro.

Você precisa de algum mecanismo para propagar o que muda na View.

Algumas das opções incluem RxSwift, que é uma biblioteca bastante grande e leva algum tempo para se acostumar.

ViewModel pode estar disparando NSNotification s em cada alteração de valor de propriedade, mas isso adiciona muito código que precisa de tratamento adicional, como assinar notificações e cancelar a assinatura quando a exibição é desalocada.

Key-Value-Observing (KVO) é outra opção, mas os usuários confirmarão que sua API não é sofisticada.

Neste tutorial, você usará genéricos e encerramentos do Swift, que estão bem descritos no artigo Bindings, Generics, Swift and MVVM.

Agora, vamos voltar ao aplicativo de exemplo.

Vá para o grupo de projetos ViewModel e crie um novo arquivo Swift, Dynamic.swift .

 class Dynamic<T> { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

Você usará essa classe para propriedades em seus ViewModels que espera alterar durante o ciclo de vida da View.

Primeiro, comece com o PlayerScoreboardMoveEditorView e seu ViewModel, PlayerScoreboardMoveEditorViewModel .

Abra PlayerScoreboardMoveEditorViewModel e observe suas propriedades.

Como não se espera que o playerName mude, você pode deixá-lo como está.

As outras cinco propriedades (cinco tipos de movimento) mudarão, então você precisa fazer algo sobre isso. A solução? A classe Dynamic acima mencionada que você acabou de adicionar ao projeto.

Dentro PlayerScoreboardMoveEditorViewModel remova as definições para cinco Strings que representam contagens de movimento e substitua por isso:

 var onePointMoveCount: Dynamic<String> { get } var twoPointMoveCount: Dynamic<String> { get } var assistMoveCount: Dynamic<String> { get } var reboundMoveCount: Dynamic<String> { get } var foulMoveCount: Dynamic<String> { get }

É assim que o protocolo ViewModel deve ficar agora:

 import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic<String> { get } var twoPointMoveCount: Dynamic<String> { get } var assistMoveCount: Dynamic<String> { get } var reboundMoveCount: Dynamic<String> { get } var foulMoveCount: Dynamic<String> { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

Esse tipo Dynamic permite alterar o valor dessa determinada propriedade e, ao mesmo tempo, notificar o objeto change-ouvinte, que, neste caso, será a View.

Agora, atualize a implementação real do ViewModel PlayerScoreboardMoveEditorViewModelFromPlayer .

Substitua isso:

 var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

com o seguinte:

 let onePointMoveCount: Dynamic<String> let twoPointMoveCount: Dynamic<String> let assistMoveCount: Dynamic<String> let reboundMoveCount: Dynamic<String> let foulMoveCount: Dynamic<String>

Nota: Não há problema em declarar essas propriedades como constantes com let pois você não alterará a propriedade real. Você alterará a propriedade value no objeto Dynamic .

Agora, há erros de compilação porque você não inicializou seus objetos Dynamic .

Dentro do método init de PlayerScoreboardMoveEditorViewModelFromPlayer , substitua a inicialização das propriedades de movimento por isto:

 self.onePointMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .onePoint))") self.twoPointMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .twoPoints))") self.assistMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .assist))") self.reboundMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .rebound))") self.foulMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .foul))")

Dentro de PlayerScoreboardMoveEditorViewModelFromPlayer vá para o método makeMove e substitua-o pelo seguinte código:

 fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = "\(game.playerMoveCount(for: player, move: .onePoint))" twoPointMoveCount.value = "\(game.playerMoveCount(for: player, move: .twoPoints))" assistMoveCount.value = "\(game.playerMoveCount(for: player, move: .assist))" reboundMoveCount.value = "\(game.playerMoveCount(for: player, move: .rebound))" foulMoveCount.value = "\(game.playerMoveCount(for: player, move: .foul))" }

Como você pode ver, você criou instâncias da classe Dynamic e atribuiu valores String a ela. Quando precisar atualizar os dados, não altere a própria propriedade Dynamic ; em vez disso, atualize sua propriedade de value .

Excelente! PlayerScoreboardMoveEditorViewModel agora é dinâmico.

Vamos aproveitá-lo e ir para a visualização que realmente ouvirá essas alterações.

Abra PlayerScoreboardMoveEditorView e seu método fillUI (você deve ver erros de compilação neste método neste momento, pois está tentando atribuir o valor String ao tipo de objeto Dynamic .)

Substitua as linhas “erradas”:

 self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

com o seguinte:

 viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Em seguida, implemente os cinco métodos que representam ações de movimento (seção Button Action ):

 @IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

Execute o aplicativo e clique em alguns botões de movimento. Você verá como os valores do contador dentro das visualizações do player mudam quando você clica no botão de ação.

Aplicativo iOS

Você terminou com PlayerScoreboardMoveEditorView e PlayerScoreboardMoveEditorViewModel .

Isso era simples.

Agora, você precisa fazer o mesmo com sua visualização principal ( GameScoreboardEditorViewController ).

Primeiro, abra GameScoreboardEditorViewModel e veja quais valores devem ser alterados durante o ciclo de vida da visualização.

Substitua as definições time , score , isFinished , isPaused pelas versões Dynamic :

 import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic<String> { get } var score: Dynamic<String> { get } var isFinished: Dynamic<Bool> { get } var isPaused: Dynamic<Bool> { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

Vá para a implementação do ViewModel ( GameScoreboardEditorViewModelFromGame ) e faça o mesmo com as propriedades declaradas no protocolo.

Substitua isso:

 var time: String var score: String var isFinished: Bool var isPaused: Bool

com o seguinte:

 let time: Dynamic<String> let score: Dynamic<String> let isFinished: Dynamic<Bool> let isPaused: Dynamic<Bool>

Você receberá alguns erros agora, porque alterou o tipo de ViewModel de String e Bool para Dynamic<String> e Dynamic<Bool> .

Vamos consertar isso.

Corrija o método togglePause substituindo-o pelo seguinte:

 func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

Observe como a única alteração é que você não define mais o valor da propriedade diretamente na propriedade. Em vez disso, você a define na propriedade value do objeto.

Agora, corrija o método initWithGame substituindo isto:

 self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

com o seguinte:

 self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

Você deve entender o ponto agora.

Você está envolvendo os valores primitivos, como String , Int e Bool , com versões Dynamic<T> desses objetos, que fornecem o mecanismo de associação leve.

Você tem mais um erro para corrigir.

No método startTimer , substitua a linha de erro por:

 self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

Você atualizou seu ViewModel para ser dinâmico, assim como fez com o ViewModel do player. Mas você ainda precisa atualizar seu View ( GameScoreboardEditorViewController ).

Substitua todo o método fillUI por este:

 fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? "Start" : "Pause" self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

A única diferença é que você alterou suas quatro propriedades dinâmicas e adicionou ouvintes de alteração a cada uma delas.

Neste ponto, se você executar seu aplicativo, alternar o botão Iniciar/Pausar iniciará e pausará o cronômetro do jogo. Isso é usado para tempos limite durante o jogo.

Você está quase terminando, exceto que a pontuação não muda na interface do usuário, quando você pressiona um dos botões de ponto (botão 1 e 2 pontos).

Isso ocorre porque você realmente não propagou as alterações de pontuação no objeto do modelo Game subjacente até o ViewModel.

Então, abra o objeto de modelo de Game para um pequeno exame. Verifique seu método updateScore .

 fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

Este método faz duas coisas importantes.

Primeiro, ele define a propriedade isFinished como true se o jogo terminar com base nas pontuações de ambas as equipes.

Depois disso, ele publica uma notificação de que a pontuação foi alterada. Você ouvirá essa notificação no GameScoreboardEditorViewModelFromGame e atualizará o valor da pontuação dinâmica no método do manipulador de notificações.

Add this line at the bottom of initWithGame method (don't forget the super.init() call to avoid errors):

 super.init() subscribeToNotifications()

Below initWithGame method, add deinit method, since you want to do the cleanup properly and avoid crashes caused by the NotificationCenter .

 deinit { unsubscribeFromNotifications() }

Finally, add the implementations of these methods. Add this section right below the deinit method:

 // MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

Now, run the app, and click on the player views to change scores. Since you've already connected dynamic score and isFinished in the ViewModel with the View, everything should work when you change the score value inside the ViewModel.

How to Further Improve the App

While there's always room for improvement, this is out of scope of this tutorial.

For example, we do not stop the time automatically when the game is over (when one of the teams reaches 15 points), we just hide the player views.

You can play with the app if you like and upgrade it to have a “game creator” view, which would create a game, assign team names, assign player names and create a Game object that could be used to present GameScoreboardEditorViewController .

We can create another “game list” view that uses the UITableView to show multiple games in progress with some detailed info in the table cell. In cell select, we can show the GameScoreboardEditorViewController with the selected Game .

The GameLibrary has already been implemented. Just remember to pass that library reference to the ViewModel objects in their initializer. For example, “game creator's” ViewModel would need to have an instance of GameLibrary passed through the initializer so that it would be able to insert the created Game object into the library. “Game list's” ViewModel would also need this reference to fetch all games from the library, which will be needed by the UITableView.

The idea is to hide all of the dirty (non-UI) work inside the ViewModel and have the UI (View) only act with prepared presentation data.

What now?

After you get used to the MVVM, you can further improve it by using Uncle Bob's Clean Architecture rules.

An additional good read is a three-part tutorial on Android architecture:

  • Android Architecture: Part 1 – Every new beginning is hard,
  • Android Architecture: Part 2 – The clean architecture,
  • Android Architecture: Part 2 – The clean architecture.

Examples are written in Java (for Android), and if you are familiar with Java (which is much closer to Swift then Objective-C is to Java), you'll get ideas on how to further refactor your code inside the ViewModel objects so that they don't import any iOS modules ( UIKit or CoreLocation eg).

These iOS modules can be hidden behind the pure NSObjects , which is good for code reusability.

MVVM is a good choice for most iOS apps, and hopefully, you will give it a try in your next project. Or, try it in your current project when you are creating a UIViewController .

Related: Working With Static Patterns: A Swift MVVM Tutorial