← Voltar para o blog

Swift não é tão difícil quanto dizem

Swift não é uma linguagem trivial, mas também não é o problema gigantesco que muita gente imagina olhando de fora. Na prática, o que costuma doer não é escrever let, guard ou struct. O que dói é entender o ecossistema Apple, estado de tela, ciclo de vida, concorrência, memória, arquitetura e manutenção de app real.

O problema quase nunca é a sintaxe

Quando alguém fala que Swift é difícil, geralmente mistura três coisas diferentes: a linguagem Swift, os frameworks da Apple e a forma como apps iOS envelhecem. São problemas relacionados, mas não são a mesma coisa.

A linguagem tem suas escolhas estranhas, claro. Optionals incomodam no começo. Closures podem ficar ilegíveis. Generics e protocols podem virar uma abstração bonita demais para um problema simples. Mas, para construir features do dia a dia, Swift é direto: tipos fortes, boas mensagens do compilador, APIs expressivas e uma biblioteca padrão pequena o bastante para você não se perder.

O susto vem quando o código deixa de ser exemplo de tutorial e vira produto: uma tela que precisa carregar dados, lidar com erro, cancelar request, atualizar estado, manter compatibilidade, respeitar o ciclo de vida e não quebrar quando o usuário faz algo fora do caminho feliz.

Optionals são chatos por um bom motivo

Optionals são uma das primeiras barreiras. E aqui vai uma opinião prática: quem odeia optional normalmente ainda está tentando escrever Swift como se estivesse em uma linguagem onde null aparece por surpresa.

Em app real, dado ausente não é detalhe. API pode mandar campo nulo, usuário pode não ter foto, localização pode não estar autorizada, cache pode estar vazio. Swift só te obriga a admitir isso no tipo.

struct UserDTO: Decodable {
    let id: String
    let name: String?
    let avatarURL: URL?
}

struct UserViewData {
    let title: String
    let avatarURL: URL?

    init(dto: UserDTO) {
        let cleanedName = dto.name?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .flatMap { $0.isEmpty ? nil : $0 }

        self.title = cleanedName ?? "Sem nome"
        self.avatarURL = dto.avatarURL
    }
}

Esse exemplo não é sobre sintaxe bonita. É sobre deixar claro onde a bagunça do mundo externo entra no app e onde ela é normalizada para a UI. Depois que você começa a pensar assim, optional deixa de ser burocracia e vira uma ferramenta de modelagem.

guard muda a leitura do código

Um padrão que deixa Swift mais agradável é sair cedo quando uma condição obrigatória não existe. Em vez de aninhar if até o código virar uma escada, você declara as pré-condições no começo.

func openProfile(from notification: Notification) {
    guard
        let userInfo = notification.userInfo,
        let userID = userInfo["user_id"] as? String,
        !userID.isEmpty
    else {
        logger.warning("Profile notification without user_id")
        return
    }

    router.openProfile(userID: userID)
}

Isso é uma das coisas que eu mais gosto na linguagem: ela favorece código que separa validação, transformação e ação. Ainda dá para escrever código ruim, mas a linguagem oferece caminhos bons.

O difícil começa quando aparece estado

Um app simples parece fácil porque quase tudo acontece em sequência. Um app real tem tela carregando, request em andamento, usuário tocando em botões, push notification chegando, cache antigo, token expirado e uma UI que precisa continuar coerente.

Swift não resolve isso sozinho. SwiftUI também não. UIKit também não. A dificuldade está em decidir onde o estado mora, quem pode alterar esse estado e como a tela reage.

@MainActor
final class ProfileViewModel: ObservableObject {
    enum State {
        case idle
        case loading
        case loaded(Profile)
        case failed(String)
    }

    @Published private(set) var state: State = .idle

    private let service: ProfileService

    init(service: ProfileService) {
        self.service = service
    }

    func load(userID: String) async {
        state = .loading

        do {
            let profile = try await service.fetchProfile(userID: userID)
            state = .loaded(profile)
        } catch {
            state = .failed("Não foi possível carregar o perfil.")
        }
    }
}

Esse tipo de código não é avançado por causa da sintaxe. Ele é importante porque explicita as fases da tela. Sem isso, a tendência é espalhar isLoading, errorMessage, data e didLoad em vários lugares até ninguém saber qual combinação é válida.

Closures, ARC e o primeiro bug invisível

Closures não são difíceis porque têm chaves no final da chamada. Elas ficam difíceis quando capturam objetos e você precisa entender ciclo de vida.

final class CheckoutViewController: UIViewController {
    private let service: CheckoutService

    init(service: CheckoutService) {
        self.service = service
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func confirmOrder() {
        service.confirm { [weak self] result in
            guard let self else { return }

            switch result {
            case .success:
                self.showSuccess()
            case .failure(let error):
                self.showError(error)
            }
        }
    }
}

Aqui entra uma parte que tutorial raso quase nunca explica bem: [weak self] não é enfeite. É uma decisão sobre posse. Se o serviço segura a closure e a closure segura a controller, você pode criar um ciclo de retenção. O app não quebra na hora. Ele só deixa objetos vivos quando deveriam morrer. Esse é o tipo de bug que faz Swift parecer "difícil", mas na verdade é desenvolvimento iOS exigindo noção de memória.

UIKit e SwiftUI têm dificuldades diferentes

UIKit é explícito. Você sabe quando cria uma view, quando configura constraint, quando atualiza uma label. O preço é escrever mais código e organizar melhor a casa.

SwiftUI é declarativo e produtivo, mas exige outra cabeça. A tela é função do estado. Quando o estado está mal modelado, a UI começa a se comportar de forma estranha: renderizações inesperadas, navegação duplicada, task rodando de novo, binding vazando responsabilidade. SwiftUI não é "mais fácil"; ele só troca um conjunto de problemas por outro.

Protocols são bons até virarem religião

Swift incentiva muito o uso de protocols. Isso é ótimo para separar contratos, testar dependências e evitar acoplamento. Mas também é fácil cair no exagero: protocol para tudo, generic para tudo, arquitetura com cinco camadas antes de existir um problema real.

protocol AppStoreStatusFetching {
    func fetchStatus(appID: String) async throws -> AppStoreStatus
}

final class AppStoreStatusService: AppStoreStatusFetching {
    func fetchStatus(appID: String) async throws -> AppStoreStatus {
        // Chamada real para a API
    }
}

Esse protocol faz sentido se existe teste, mock, variação de implementação ou fronteira clara de domínio. Se ele só existe porque "arquitetura limpa manda", talvez seja só ruído. Swift dá ferramentas fortes, mas maturidade é saber quando não usar.

Uma ordem melhor para aprender

Se eu fosse orientar alguém começando hoje, não começaria por property wrappers, macros, actors ou arquitetura. Eu seguiria esta ordem:

  1. Tipos e controle de fluxo: let, var, struct, enum, switch, funções e coleções.
  2. Optionals sem atalhos perigosos: if let, guard let, ?? e evitar ! como hábito.
  3. Modelagem de estado: enums para estados de tela, dados imutáveis quando possível e responsabilidades pequenas.
  4. UIKit ou SwiftUI com foco em ciclo de vida: entender quando a tela nasce, atualiza, desaparece e libera memória.
  5. Concorrência: async/await, @MainActor, cancelamento e atualização de UI na thread correta.
  6. Arquitetura só depois de sentir a dor: MVVM, coordinators, services, repositories e protocols precisam responder a problemas reais.

Então Swift é fácil?

Não exatamente. Swift é uma linguagem com profundidade. Dá para passar anos usando e ainda aprender detalhes de generics, concurrency, memory exclusivity, result builders, property wrappers e performance.

Mas Swift é honesto. Ele te cobra clareza: esse valor pode ser nulo? Esse código roda na main thread? Quem é dono desse objeto? Esse estado tem quais possibilidades? Quando você responde essas perguntas no código, o app fica mais previsível.

O que eu diria para quem está começando é: não tenha medo da linguagem. Tenha respeito pelo ecossistema. Aprender Swift é uma parte do caminho; aprender a construir, publicar, manter e evoluir app iOS é o trabalho de verdade.

Está começando com Swift? Me chama no LinkedIn. Se a dúvida for real e específica, eu provavelmente vou gostar de discutir.