Executando verificação de segurança...
3

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 dado
  • ViewModels – lógica de apresentação e regras de negócio
  • Views – 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

  1. Cmd+Shift+U para abrir o simulador.
  2. Espere o loading desaparecer.
  3. 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 @StateObject por @EnvironmentObject para compartilhar a mesma ViewModel entre telas.
  • Cache: salve respostas em UserDefaults ou CoreData dentro da própria ViewModel.
  • Testes: crie RepoListViewModelTests usando XCTest e mocks de GitHubService gerados 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ã.

Carregando publicação patrocinada...