Engenharia iOS

[Arquitetura iOS #3] O SwiftUI precisa de ViewModel? Debate completo sobre padrões MV

A comunidade iOS mantém uma discussão acalorada há vários anos.

5 min de leitura
Imagem de capa de [Arquitetura iOS #3] O SwiftUI precisa de ViewModel? Debate completo sobre padrões MV

A comunidade iOS mantém uma discussão acalorada há vários anos.

“O SwiftUI precisa de ViewModel?”

No artigo anterior, concluímos que o binding é o núcleo do MVVM. O SwiftUI já traz o binding integrado ao framework. Então surgiu o argumento de que a própria camada ViewModel seria redundante. Esse é o campo do padrão MV (Model-View).

No último artigo da série, vamos analisar os argumentos dos dois lados com equilíbrio e resumir os critérios práticos para decidir.


A View do SwiftUI já se parece com um ViewModel

Relembrando por que o UIKit precisava de MVVM, os motivos são claros.

  • O UIViewController ficou grande demais, então era preciso remover o estado e a lógica
  • Era necessário usar binding para sincronizar o estado e a tela

Mas a View do SwiftUI já é, por natureza, uma função do estado. bodyEla recebe o estado e retorna a declaração da tela; quando o @State valor muda, o framework redesenha tudo automaticamente. Não é necessário escrever código de assinatura do Combine para fazer binding.

Além disso, a View do SwiftUI é uma struct de tipo por valor, não uma classe. Em vez de ser um objeto de ciclo de vida que pode crescer para milhares de linhas como um UIViewController, ela se parece mais com uma planta leve recriada a cada renderização.

É daí que parte o argumento do grupo MV: se a View não fica enorme e o binding é gratuito, criar uma classe ViewModel para cada tela não seria apenas carregar por inércia os hábitos do UIKit?


Argumentos do grupo MV

No padrão MV, não há um ViewModel por tela; a View usa diretamente as ferramentas de estado.

  • O estado restrito a uma única View fica @Stateali
  • O estado compartilhado por várias telas é injetado como @Observablemodelo@Environment
  • Os stores ou clients da camada Model cuidam dos dados do servidor
struct ProfileView: View {
    @Environment(UserStore.self) private var store
    @State private var isEditing = false

    var body: some View {
        List(store.posts) { PostRow(post: $0) }
            .task { await store.loadPosts() }
    }
}

Os exemplos oficiais da Apple, como Fruta e Food Truck, geralmente seguem essa estrutura. Em vez de um ViewModel por tela, várias Views compartilham um único @Observablemodelo de domínio.

As vantagens são claras: desaparece o boilerplate de criar uma classe por tela, e ferramentas do framework como @State e @Binding podem ser usadas diretamente.

A View do SwiftUI já é uma função do estado
A View do SwiftUI já é uma função do estado

A réplica do grupo MVVM

Os argumentos do outro lado também são fortes.

Primeiro, a lógica se infiltra na View. Quando condições como “mostrar uma mensagem se não houver posts, o carregamento tiver terminado e não houver erro” começam a se acumular como operadores ternários dentro de body, até um tipo por valor fica difícil de ler.

Segundo, o problema dos testes. O body da View é difícil de executar com testes unitários. Se a lógica de apresentação está dentro da View, ela também fica fora do alcance dos testes. Ao extraí-la para um ViewModel, ela vira um objeto Swift puro que pode ser validado diretamente.

Terceiro, a escala. Em um app com dezenas de telas e várias pessoas trabalhando, é preciso ter regras consistentes sobre onde ficam o estado e a lógica. Ter um lugar definido chamado ViewModel para cada tela pode reduzir o custo de colaboração.


A direção das dependências, não o nome

Observando os dois argumentos, a discussão é, na verdade, sobre onde vivem a lógica e o estado, e não sobre existir ou não uma classe chamada ViewModel. Com esse critério, a maioria dos casos fica clara.

  • O estado de UI restrito a uma única View, como toggle, foco e visibilidade de sheet, deve ficar @Statena View. Extraí-lo para um ViewModel é excesso de engenharia.
  • O estado de domínio compartilhado por várias telas deve ser criado como @Observablemodelo e injetado. Store ou ViewModel: o nome não muda a essência.
  • Quando a lógica de transformação específica da tela fica complexa —formatação, condições de exibição, ordenação e filtragem—, extraia-a para um tipo testável fora da View. Isso é, na prática, um ViewModel.
  • De qualquer forma, se você mantiver a direção das dependências e garantir que a View não acesse diretamente a rede ou o banco de dados, MV e MVVM são quase apenas nomes diferentes.

No fim, “SwiftUI não precisa de ViewModel” pode ser resumido assim: não é necessário criar um ViewModel mecanicamente para cada tela, mas isso não significa que desapareça a lógica que deve sair da View.

O critério real é a direção das dependências, não o nome
O critério real é a direção das dependências, não o nome

Resumo da série

A série de arquitetura iOS em três partes pode ser resumida em uma linha por tópico.

  • MVC: UIViewController acumula as funções de View e Controller, concentrando responsabilidades. Ainda funciona bem para telas pequenas.
  • MVVM: remove o estado da tela para um ViewModel e o sincroniza por binding. Sem binding, fica pela metade.
  • O debate sobre MV: o SwiftUI já tem binding integrado, então não há motivo para impor um ViewModel em cada tela. Ainda assim, é preciso projetar onde a lógica fica e qual é a direção das dependências.

Arquitetura não é moda; é uma escolha de trade-offs adequada ao tamanho da equipe e do app. Se houver interesse, também abordaremos opções mais pesadas, como VIPER e TCA.

Continue lendo