Do zero ao app: criando seu primeiro projeto com SwiftUI e MVVM
Semana passada a Apple liberou o Xcode 15.3 com melhorias no Preview e novas APIs para Combine. Embora o lançamento tenha sido discreto, ele reacendeu a discussão: qual é a forma mais simples de começar um app iOS hoje sem se perder em padrões arquiteturais? A resposta curta: SwiftUI + MVVM é o combo que mais tem acelerado a entrada de novos desenvolvedores no ecossistema iOS. Neste guia, vamos juntar a simplicidade do SwiftUI com a clareza do MVVM para tirar sua ideia do papel em menos de uma tarde.
Por que SwiftUI + MVVM?
SwiftUI eliminou boa parte do “código burocrático” que assustava iniciantes: storyboards, outlets, delegates. MVVM, por sua vez, organiza o código de modo que você saiba exatamente onde colocar cada nova funcionalidade. Juntos, eles formam um casal que permite evoluir um protótipo em produção sem grandes rewrites.
Imagine que você quer listar repositórios do GitHub. Com UIKit tradicional seriam pelo menos três arquivos (storyboard, view controller, model). No SwiftUI com MVVM você precisa de:
- Um modelo simples (struct)
- Uma ViewModel que faz a chamada de rede
- Uma View declarativa que reflete estados (loading, sucesso, erro)
Estrutura mínima de pastas
Dentro do seu projeto Xcode, crie três grupos:
Models– onde vivem seus tipos de dadoViewModels– lógica de apresentação e regras de negócioViews– tudo o que aparece na tela
Essa divisão não é oficial da Apple, mas é seguida por 90 % dos times que migram para SwiftUI. Ela facilita code-reviews e onboarding de novos desenvolvedores.
Modelo: a camada que nunca muda
// Models/Repo.swift
struct Repo: Identifiable, Decodable {
let id: Int
let name: String
let description: String?
}
O protocolo Identifiable é exigido pelo List do SwiftUI; Decodable permite decodar JSON diretamente.
ViewModel: onde a mágica acontece
// ViewModels/RepoListViewModel.swift
import Combine
import Foundation
@MainActor
class RepoListViewModel: ObservableObject {
@Published var repos: [Repo] = []
@Published var isLoading = false
@Published var errorMessage: String?
private let service = GitHubService()
func fetch() async {
isLoading = true
errorMessage = nil
do {
repos = try await service.getRepos()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
@MainActor garante que todas as mutações de estado ocorram na thread principal, evitando crashes clássicos. O uso de async/await (novidade desde o Swift 5.5) deixa o código linear, sem callbacks aninhados.
View: código que parece HTML mas é Swift
// Views/RepoListView.swift
import SwiftUI
struct RepoListView: View {
@StateObject private var vm = RepoListViewModel()
var body: some View {
NavigationStack {
Group {
if vm.isLoading {
ProgressView("Buscando…")
} else if let msg = vm.errorMessage {
Text(msg).foregroundStyle(.red)
} else {
List(vm.repos) { repo in
VStack(alignment: .leading) {
Text(repo.name).font(.headline)
if let desc = repo.description {
Text(desc).font(.subheadline)
}
}
}
}
}
.navigationTitle("Repositórios")
.task { await vm.fetch() } // executa quando a view aparece
}
}
}
.task { … } substitui o antigo onAppear; ele é cancelado automaticamente quando a View desaparece, evitando memory leaks.
Camada de rede isolada
// Services/GitHubService.swift
import Foundation
actor GitHubService {
func getRepos() async throws -> [Repo] {
let url = URL(string: "https://api.github.com/users/apple/repos")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Repo].self, from: data)
}
}
Usar actor em vez de class é uma prática nova que elimina concorrência: a função só pode ser chamada por uma task por vez, sem precisar de locks manuais.
Testando o fluxo inteiro em 3 cliques
- Cmd+Shift+U para abrir o simulador.
- Espere o loading desaparecer.
- Veja a lista surgir.
Sem configurar storyboards ou outlets.
Dica de ouro: Preview que poupam horas
struct RepoListView_Previews: PreviewProvider {
static var previews: some View {
let vm = RepoListViewModel()
vm.repos = [.mock, .mock2]
return RepoListView()
.environmentObject(vm)
}
}
extension Repo {
static var mock = Repo(id: 1, name: "Swift", description: "The Swift Programming Language")
static var mock2 = Repo(id: 2, name: "SwiftUI", description: nil)
}
Assim você visualiza diferentes estados (loading, erro, dados) no canvas lateral do Xcode sem rodar o simulador.
Evoluindo sem medo: próximos passos
- Injetar dependências: troque
@StateObjectpor@EnvironmentObjectpara compartilhar a mesma ViewModel entre telas. - Cache: salve respostas em
UserDefaultsou CoreData dentro da própria ViewModel. - Testes: crie
RepoListViewModelTestsusandoXCTeste mocks deGitHubServicegerados automaticamente com protocolos.
Quando não usar MVVM em SwiftUI?
Se seu app tem somente uma tela e não faz chamadas de rede, um @State isolado dentro da View resolve. MVVM começa a valer a pena quando você tem:
- Múltiplas Views que dependem do mesmo estado
- Regras de negócio crescentes
- Necessidade de testes unitários
Conclusão
A combinação SwiftUI + MVVM não é mais o futuro: é o presente que a Apple entregou de bandeja. Com apenas três arquivos você tem um app funcional, testável e pronto para escalar. A próxima vez que surgir uma ideia, reserve 60 minutos, abra o Xcode 15.3 e siga os passos acima. Seu protótipo estará rodando antes do café esfriar — e, o mais importante, o código que você escrever hoje continuará legível quando a feature crescer amanhã.