iOS Inside #004 – Arquitetura iOS em 2025: do caos ao Clean com propósito

iOS InsideiOS Inside
12 min read

This article is also available in English:
https://iosinside.hashnode.dev/ios-inside-004-ios-app-architecture-in-2025-from-chaos-to-clean-with-purpose

Você provavelmente já ouviu falar em MVC, MVVM, Clean Architecture, VIPER, TCA, Redux...
Talvez até tenha tentado aplicar algum deles no seu projeto. E, no meio do caminho, se pegou pensando:
“Será que eu tô complicando demais? Ou será que tô fazendo errado?”

A real é que a gente, como dev iOS, muitas vezes entra numa batalha entre o código que precisa funcionar hoje e a arquitetura que promete salvar o amanhã.

Este artigo é sobre isso.

Não é sobre qual padrão é o “melhor”.
É sobre entender o que realmente importa quando o assunto é estruturar um app iOS em 2025.
E mais importante: como fazer isso de forma realista, com propósito, e sem virar refém da teoria.

Por que falar de arquitetura agora?

Porque os apps estão ficando mais complexos.
Porque o Swift evoluiu (e muito).
E porque o Xcode não vai te salvar de uma ViewController com 800 linhas.

Arquitetura importa quando:

  • O time cresce

  • O código precisa durar

  • As funcionalidades começam a se cruzar

  • A manutenção vira um pesadelo

  • O onboarding de novos devs demora mais do que devia

E talvez o mais importante:
Arquitetura importa quando você quer ter paz de espírito ao abrir seu projeto daqui 6 meses.

O cenário em 2025: o que tá rolando na prática?

Se você abrir 10 projetos iOS hoje, provavelmente vai encontrar:

  • Um pouco de MVC (com ViewControllers inchadas)

  • Um MVVM com variações (às vezes usando Combine, às vezes só closures)

  • Uma tentativa de Clean Architecture (nem sempre bem isolada)

  • E cada vez mais, uma adoção parcial ou total do TCA (The Composable Architecture)

Ou seja:
a maioria dos projetos está em algum tipo de “zona cinzenta” arquitetural.
Nem 100% purista, nem totalmente caótico. E tá tudo bem.

A grande questão é:
Como fazer isso direito? Como manter flexível, escalável, compreensível?

As arquiteturas que você vai cruzar em 2025 (e o que pensar sobre elas)

Vamos deixar algo claro desde já: nenhuma arquitetura é perfeita.
Cada uma resolve um tipo de problema — e traz novas dores junto.

A seguir, um panorama honesto e direto das abordagens mais comuns (e que você provavelmente vai encontrar ou considerar nos próximos anos):

MVC (Model-View-Controller)
É o padrão original do iOS, simples de entender, rápido de colocar em prática — e também o mais perigoso quando mal usado.

O que tem de bom:

  • Baixa curva de aprendizado

  • Ideal pra protótipos ou apps muito pequenos

  • Funciona bem com Storyboards e UIKit puro

Onde quebra:

  • ViewController inchada, com lógica de UI, rede, parsing e navegação tudo junto

  • Dificuldade pra testar

  • A lógica de negócio fica dispersa

Usar MVC em 2025 é possível?
Sim. Mas com limites claros, extraindo responsabilidades desde cedo.

MVVM (Model-View-ViewModel)

O favorito de quem usa SwiftUI, mas também muito usado com UIKit + Combine ou RxSwift.

O que tem de bom:

  • Separa lógica de apresentação da View

  • Funciona bem com binding (Combine, SwiftUI)

  • Ajuda a reaproveitar lógica entre telas

Onde quebra:

  • ViewModel inflado demais (quase um controller disfarçado)

  • Quando vira um “dump” de lógica sem critério

  • Comunicação entre camadas mal definida

MVVM brilha quando você domina observables, bindings, e consegue manter a ViewModel enxuta.
Caso contrário, vira só um MVC com outro nome.

Clean Architecture (Uncle Bob Style)

O ideal dos idealistas.
Organiza o app em camadas bem separadas (Domain, Data, UI) com boundaries fortes e independência de frameworks.

O que tem de bom:

  • Clareza sobre onde cada responsabilidade vive

  • Facilita testes e manutenção a longo prazo

  • Ajuda a escalar times e features de forma independente

Onde quebra:

  • Complexidade inicial (pra apps pequenos pode ser overkill)

  • A camada de “UseCases” nem sempre é bem compreendida

  • Comunicação entre camadas pode virar burocracia

Clean Architecture faz sentido quando o projeto tem ambição de crescer, especialmente se envolve múltiplas plataformas ou times.

TCA (The Composable Architecture)

Criada pela galera da Point-Free, o TCA é uma arquitetura reativa, baseada em composição, estados imutáveis e efeitos controlados.

O que tem de bom:

  • Escalável com organização clara de estado, ação e ambiente

  • Baseado em testes desde o início

  • Modular por natureza

Onde quebra:

  • Curva de aprendizado alta

  • Excesso de código pra algo simples

  • Dificuldade de onboardar devs que não estão acostumados com o padrão

TCA é excelente pra apps com lógica complexa, múltiplas features e necessidade de testabilidade forte. Mas pode assustar se mal introduzido.

VIP / VIPER

Mais comuns no mundo corporativo, especialmente em apps que vieram de Objective-C e projetos enterprise.

O que tem de bom:

  • Separação rígida de responsabilidades

  • Testes fáceis por camadas

  • Fluxos previsíveis

Onde quebra:

  • Excessivamente verboso

  • Muito boilerplate

  • Overhead grande pra features simples

VIPER funciona em apps gigantes, onde o controle de dependências e responsabilidades precisa ser extremo. Fora disso? Pode ser demais.

E se você misturar tudo isso?

Você vai.
A verdade é que a maioria dos projetos em 2025 estão usando arquiteturas híbridas. E isso não é um problema.

O que importa é ter clareza sobre os papéis, mesmo que você não siga um padrão formal.

Arquitetura com propósito: por que você está escolhendo o que está escolhendo?

Quando a gente fala de arquitetura, é comum ver discussões técnicas girando em torno de “o que é mais certo”, “qual padrão é melhor” ou “como se faz no projeto tal”.

Mas a pergunta mais honesta que você pode fazer é:
“Por que eu tô escolhendo essa abordagem?”

A melhor arquitetura não é a mais teórica.
É a que funciona pra você, pro seu time e pro momento do seu projeto.

Vamos por partes. Seu projeto precisa de:

1. Testabilidade?

Arquiteturas como MVVM bem feito, Clean ou TCA favorecem testabilidade desde o início.
Você consegue isolar lógica de negócio, injetar dependências e escrever testes unitários com mais facilidade.

Exemplo prático:
Se você tem lógica de regra de negócio crítica (tipo cálculo de preço, regras de aprovação, personalização de conteúdo), a separação da UI é vital pra conseguir testar isso com confiança.


2. Escalabilidade?

Se o app vai crescer rápido — com novos módulos, novos devs e novas integrações — você precisa pensar modular.
Clean Architecture + Modularização por Feature é uma combinação que encaixa bem aqui.

Use packages separados por domínio (ex: Auth, Home, Settings) e crie boundaries entre UI e lógica.
Dá pra começar com pastas organizadas e depois evoluir pra SPM, Tuist ou outro sistema.


3. Entregas rápidas?

Se você tá num projeto solo, MVP ou protótipo, uma abordagem mais leve pode funcionar melhor.
MVC com organização, ou um MVVM enxuto, sem camadas exageradas, é o que você precisa.

A regra é: evite overengineering.
Se o custo de manter a arquitetura for maior que o ganho... talvez você esteja forçando a barra.


4. Time pequeno (ou time júnior)?

Arquiteturas complexas exigem maturidade.
Se o time ainda tá aprendendo, talvez seja melhor começar com algo simples, com boas práticas, mas sem tanto formalismo.

Uma arquitetura mais clara e bem comentada vale mais do que uma stack que ninguém entende.


5. Compartilhamento entre plataformas?

Se você quer dividir lógica entre iOS, watchOS, visionOS (ou até macOS), o ideal é isolar ao máximo a lógica de negócio da UI.

Nesse caso, a Clean Architecture com domínio puro (sem UIKit ou SwiftUI) na camada de regras pode te dar a flexibilidade que precisa.


Um framework mental simples:

“O que eu tô tentando resolver com essa arquitetura?”

Essa pergunta simples evita que você escolha uma estrutura só porque viu num vídeo da WWDC.

Comece pelas dores reais do seu app.
Escolha o padrão que te ajuda com isso.
E implemente só o que faz sentido agora.

Modularização: o passo mais subestimado da arquitetura

Você não precisa trabalhar num super app com 100 devs pra modularizar seu projeto.
Na real, quanto menor o time, mais modularização ajuda a manter a sanidade.

O problema é que muita gente acha que modularizar significa criar 30 Swift Packages e passar 3 dias lutando com o Package.swift.

Não precisa ser assim.

Modularizar é dividir com intenção

A ideia é separar responsabilidades em blocos independentes, com limites claros. Isso facilita:

  • Testes (você isola e testa sem carregar o app inteiro)

  • Navegação e dependências (cada módulo sabe o que pode ou não acessar)

  • Leitura (devs novos entendem rápido a estrutura)

  • Refatorações futuras


Três formas práticas de modularizar

1. Por Camada

Exemplo: Core, UI, Networking, Database

Ideal pra apps pequenos e médios. Separa as responsabilidades principais do projeto, mesmo que ainda tudo esteja no mesmo target.

/Core
  - Models
  - Utils
/Networking
  - APIClient.swift
  - Endpoints.swift
/UI
  - CustomButtons.swift
  - Styles.swift

Dá pra manter tudo no mesmo projeto e ainda assim ter organização modular clara.

2. Por Domínio

Exemplo: Authentication, UserProfile, Feed

Foca em agrupar lógica e telas que pertencem a uma mesma funcionalidade.

/Auth
  - AuthView.swift
  - AuthViewModel.swift
  - AuthService.swift
/Profile
  - ProfileView.swift
  - ProfileViewModel.swift
  - ProfileRouter.swift

Funciona muito bem pra times pequenos com features independentes.

3. Por Feature + SPM (ou Tuist)

Exemplo: Pacotes separados que são plugáveis no app

Aqui a coisa fica mais avançada — você transforma cada parte do app em um Swift Package ou módulo.

/Features
  /HomeFeature
    - HomeView.swift
    - HomeViewModel.swift
    - HomeUseCase.swift
  /SettingsFeature
    - ...

E ainda separa Core, SharedUI, DesignSystem, Networking em pacotes reutilizáveis.

Essa estrutura escala.
Mas também exige alinhamento de time, automação e padronização.


Ferramentas que ajudam:

  • Swift Package Manager — nativo, leve, fácil de manter

  • Tuist — perfeito pra times com múltiplos targets e builds complexos

  • XcodeGen — bom pra quem curte manter o projeto versionado por YAML

  • Manual (sem automation) — serve também, especialmente no começo


Comece leve

Você não precisa modularizar tudo de uma vez.
Escolha uma área do app que você quer isolar e transforma isso num módulo, pasta ou package separado.

Exemplo prático:

Crie um DesignSystem com seus botões, cores, fontes.
Ou isole o Networking e torne ele reutilizável.

Com o tempo, o projeto vai se ajustando. E modularização vira parte natural do fluxo.

Exemplo real: do caos ao Clean com propósito

Vamos imaginar um app simples com uma tela de login.

Cenário comum: tudo na ViewController

class LoginViewController: UIViewController {

    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func didTapLogin(_ sender: UIButton) {
        let email = emailTextField.text ?? ""
        let password = passwordTextField.text ?? ""

        let body = ["email": email, "password": password]
        let url = URL(string: "https://api.meuapp.com/login")!

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)

        URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data {
                print("Success: \(data)")
            }
        }.resume()
    }
}

Lógica de UI, rede, parsing e fluxo tudo junto.
Testar isso? Boa sorte. Reaproveitar? Impossível. Refatorar? Com medo.


Agora, vamos ver a mesma funcionalidade com separação clara:

ViewController

class LoginViewController: UIViewController {
    var viewModel = LoginViewModel()

    @IBAction func didTapLogin(_ sender: UIButton) {
        viewModel.login(email: emailTextField.text ?? "", password: passwordTextField.text ?? "")
    }
}

ViewModel

class LoginViewModel {
    private let loginUseCase = LoginUseCase()

    func login(email: String, password: String) {
        loginUseCase.execute(email: email, password: password) { result in
            switch result {
            case .success(let token):
                print("Token recebido: \(token)")
            case .failure(let error):
                print("Erro: \(error)")
            }
        }
    }
}

UseCase

class LoginUseCase {
    private let authService = AuthService()

    func execute(email: String, password: String, completion: @escaping (Result<String, Error>) -> Void) {
        authService.login(email: email, password: password, completion: completion)
    }
}

Service

class AuthService {
    func login(email: String, password: String, completion: @escaping (Result<String, Error>) -> Void) {
        // Requisição de rede aqui
    }
}

O que mudou?

  • Cada classe tem uma responsabilidade só

  • A lógica de rede tá isolada

  • A ViewModel organiza o fluxo

  • O UseCase permite testes unitários fáceis

  • A ViewController voltou a ser só... a view


Testando agora ficou fácil:

Você pode testar o LoginUseCase sem depender de UI, mocks ou endpoints reais.
Isso abre portas pra testes automatizados e TDD.

Como aplicar tudo isso no seu projeto HOJE

Ok, você leu até aqui.
Talvez já esteja pensando:
“Legal, mas meu app já tá em produção. E tá longe de estar modular ou limpo.”

Boa notícia: você não precisa parar tudo nem começar do zero.
Você pode começar pequeno, com intenção, e evoluir sua arquitetura no dia a dia.

Passo 1 – Organize as pastas

Antes de pensar em Swift Package ou Clean Architecture, organize seu projeto por contexto.

Saia do padrão “Controllers, Views, Models” e vá para algo como:

/Auth
  - AuthView.swift
  - AuthViewModel.swift
  - AuthService.swift
/Profile
  - ProfileView.swift
  - ProfileViewModel.swift
/Core
  - NetworkManager.swift
  - Constants.swift

Só de reorganizar, a compreensão do projeto já melhora muito.

Passo 2 – Extraia o que mais se repete

Comece um Shared ou Core com:

  • Botões e estilos

  • Cores e fontes

  • Lógica de rede

  • Helpers e formatadores

Isso já reduz acoplamento e prepara o terreno pra modularização real depois.

Passo 3 – Refatore por evento

Toda vez que for mexer em algo, pense:

“Eu consigo separar essa lógica aqui?”

Se sim, crie uma ViewModel, um UseCase, ou mova pra outro arquivo.
Refatorar por contexto é menos arriscado e mais sustentável do que tentar reescrever tudo de uma vez.

Passo 4 – Escolha seu estilo (com propósito)

Lembra da pergunta:

“O que eu tô tentando resolver com essa arquitetura?”

Use isso como bússola.

  • Precisa testar? Priorize separação e injeção de dependência

  • Precisa escalar? Vá modularizando por feature

  • Tá no MVP? Use MVVM leve, com organização

  • Quer dividir com outros devs? Invista em estrutura compreensível

Conclusão – Arquitetura é cultura

No fim das contas, arquitetura não é sobre diagramas ou nomes de padrões.
É sobre como seu time pensa, colabora e cuida do código.

Um código limpo só sobrevive se a cultura for limpa também.

Então da próxima vez que for escolher uma arquitetura, não pense só no que é certo “segundo o YouTube”.
Pense no que funciona pra você, hoje — e que pode crescer amanhã.


Próximo episódio?

Nesse novo formato do iOS Inside, vamos trazer temas maiores, mais profundos e sempre com o pé no chão da vida real de dev.

Se curtiu esse conteúdo, manda pra um dev iOS que merece sair do caos arquitetural.

E como sempre:
Build. Run. Refactor. E agora... com estrutura.

0
Subscribe to my newsletter

Read articles from iOS Inside directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

iOS Inside
iOS Inside