Clean Swift: como criar um aplicativo iOS e organizar o código

Clean Swift: como criar um aplicativo iOS e organizar o código

Quando começamos a criar aplicativos iOS com a linguagem Swift, é comum não planejarmos o projeto.

Geralmente, isso acontece porque os projetos são mais simples (ou somos inexperientes), porém é muito importante saber como planejar e organizar código para que futuramente as nossas aplicações tenham facilidade para evoluir.

Pense em um código desorganizado e sem planejamento como um quarto bagunçado: é difícil achar o que precisamos e fazer qualquer coisa lá dentro!

Neste artigo, vamos explorar a importância desse planejamento e entender como o Clean Swift pode contribuir com a importante tarefa de organizar o código.

Arquitetura de Software

É bem frequente, quando você analisa um software, observar que a forma como ele está estruturado não é a mais adequada.

Você poderia notar diversos problemas: a nomeação de variáveis; a declaração equivocada de classes; a forma como os componentes se conectam; e se as camadas estão cumprindo suas responsabilidades corretamente (entre outros).

Enfim, você percebe que os alicerces do projeto estão frágeis (e o risco do projeto “desmoronar” é possível). E o que acontece?

Para implementar uma nova funcionalidade, perdemos dias refatorando o código e consertando bugs. Talvez, nosso cliente ou gestor(a) diga: “mas o aplicativo funcionava antes!”.

Certamente, a falta de planejamento e organização causa diversos malefícios a um projeto.

Vamos observar um trecho de código para entender melhor como é esse código que demonstra falta de planejamento e organização (e dará problemas depois):

import Foundation

class DataService {
    // Simulação de uma requisição de dados da API
    private func requestDataFromAPI() -> Data? {
        let simulatedData = "Simulated API Data".data(using: .utf8)
        return simulatedData
    }

    func fetchData(completion: @escaping (Data?) -> Void) {
        // Requisição de dados da API
        let data = requestDataFromAPI()

        // Verifica se os dados foram recuperados
        if let data = data {
            // Armazenamento local
            UserDefaults.standard.set(data, forKey: "cachedData")
            completion(data)
        } else {
            print("Falha ao buscar dados da API.")
            completion(nil)
        }
    }

    func printCachedData() {
        // Exibindo os dados armazenados para conferência
        if let cachedData = UserDefaults.standard.data(forKey: "cachedData"),
           let cachedString = String(data: cachedData, encoding: .utf8) {
            print("Dados recuperados do cache: \(cachedString)")
        } else {
            print("Nenhum dado encontrado no cache.")
        }
    }
}

// Uso do serviço
let dataService = DataService()

dataService.fetchData { data in
    // Verifica se os dados foram obtidos
    if data != nil {
        print("Dados obtidos da API e armazenados com sucesso.")
        dataService.printCachedData() // Imprime os dados do cache
    } else {
        print("Nenhum dado disponível.")
    }
}

Nessa aplicação, a classe DataService não apenas busca dados da API, mas também lida com o armazenamento local, acumulando responsabilidades que não deveriam ser dela. Falta a separação de responsabilidades.

Embora pareça um problema pequeno em um código simples, à medida que o software cresce, o problema pequeno se torna muito grande.

A falta de separação de responsabilidades, em geral, leva a dificuldades ao implementar novas funcionalidades ou realizar alterações.

Você não quer que isso aconteça, certo?

Vamos refatorar esse código para melhorar a separação de responsabilidades:

import Foundation

class APIClient {
    func requestDataFromAPI() -> Data? {
        // Dados simulados (normalmente, aqui teria uma requisição real)
        let simulatedData = "Simulated API Data".data(using: .utf8)
        return simulatedData
    }
}

class APIService {
    private let apiClient = APIClient() // Instância de APIClient

    func fetchData(completion: @escaping (Data?) -> Void) {
        // Requisição de dados da API
        let data = apiClient.requestDataFromAPI()
        completion(data)
    }
}

class LocalStorageService {
    func saveData(_ data: Data) {
        // Armazenamento local simulado usando UserDefaults
        UserDefaults.standard.set(data, forKey: "cachedData")
        print("Dados armazenados localmente!")
    }
}

// Uso dos serviços
let apiService = APIService()
let localStorageService = LocalStorageService()

apiService.fetchData { data in
    if let data = data {
        localStorageService.saveData(data)

        // Exibindo os dados armazenados para conferência
        if let cachedData = UserDefaults.standard.data(forKey: "cachedData"),
           let cachedString = String(data: cachedData, encoding: .utf8) {
            print("Dados recuperados do cache: \(cachedString)")
        }
    } else {
        print("Falha ao buscar dados.")
    }
}

Na refatoração, a DataService foi dividida em três classes: a APIClient, que simula a requisição de dados da API com o método requestDataFromAPI; a APIService, que utiliza o APIClient no método fetchData para obter os dados; e a LocalStorageService, que armazena os dados usando UserDefaults no método saveData.

Percebe como o código ficou mais organizado e estruturado?

Se fosse necessário implementar novas funcionalidades de busca de dados ou armazenamento, teríamos um controle melhor de cada uma das camadas, e seria fácil fazer isso.

Preste atenção que, nessa refatoração, fizemos apenas a separação correta das responsabilidades de um código pequeno, mas outros problemas podem surgir, e dependendo do tamanho da aplicação, essa missão se torna bem difícil.

E é nesse momento que entra a arquitetura de software, que auxilia na organização do nosso projeto: de componentes, passando por tecnologias, aos princípios de funcionamento da aplicação!

Não é simples definir o que é uma arquitetura de software.

Mas, de forma geral, podemos dizer que a arquitetura tenta fazer com que o seu aplicativo se torne fácil de entender, desenvolver, manter, expandir e implantar.

E a arquitetura faz tudo isso trazendo organização, planejamento, estrutura e estratégia ao seu projeto.

Certo, entendemos a importância da arquitetura de software.

Mas qual arquitetura aplicar em um aplicativo iOS? Veremos isso na sequência.

Banner promocional da Alura, com um design futurista em tons de azul, apresentando o texto

Problemas das arquiteturas iOS

Você pode escolher entre várias arquiteturas para desenvolvimento de aplicativos iOS, como MVVM, MVC…Cada uma tem seus benefícios, mas, também, limitações.

Para exemplificar, vamos utilizar a arquitetura MVC (Model-View-Controller). Analisaremos um trecho de código de um aplicativo “escolar” em que adicionaremos uma funcionalidade de navegação de telas.

Model

Nessa camada, definimos um modelo simples que representa uma atividade escolar:

class Activity {
    let title: String
    let description: String 

    init(title: String, description: String) {
        self.title = title
        self.description = description
    }
}

Colocar a navegação entre telas aqui não faz sentido, já que a Model deve cuidar apenas da lógica de negócios e o armazenamento de dados

View

A view representa a interface da pessoa usuária.

import UIKit

class ActivityView: UIView {
    private var titleLabel: UILabel

    override init(frame: CGRect) {
        titleLabel = UILabel(frame: frame)
        super.init(frame: frame)
        addSubview(titleLabel)
    }

    required init?(coder: NSCoder) {
        return nil
    }

    func configure(with activity: Activity) {
        titleLabel.text = activity.title
    }
}

Poderíamos pensar em colocar a navegação na View, mas isso não é o ideal. A View deve só exibir as informações, sem se preocupar com a navegação.

Controller

Logo, a lógica de navegação foi movida para a camada Controller. Agora, o Controller é responsável por fazer a transição entre telas e preparar as informações para a View, que se concentra apenas na exibição.

import UIKit

class ActivityViewController: UIViewController {
    private var activityView: ActivityView!

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

        let activity = Activity(title: "Lição de Matemática", description: "Resolva problemas de álgebra.")
        activityView.configure(with: activity)

        setupDetailsButton()
    }

    private func setupView() {
        activityView = ActivityView(frame: view.bounds)
        view.addSubview(activityView)
    }

    private func setupDetailsButton() {
        let detailsButton = UIButton(frame: CGRect(x: 0, y: 100, width: 200, height: 50))
        detailsButton.setTitle("Ver Detalhes", for: .normal)
        detailsButton.setTitleColor(.blue, for: .normal)
        detailsButton.addTarget(self, action: #selector(navigateToDetails), for: .touchUpInside)
        view.addSubview(detailsButton)
    }

    @objc private func navigateToDetails() {
        let detailsViewController = ActivityDetailsViewController()
        present(detailsViewController, animated: true, completion: nil)
    }
}

A decisão que tomamos não foi equivocada, todavia, por conta de uma limitação de arquitetura, levamos essa responsabilidade para o Controller.

Apesar de ser fácil de implementar, com o crescimento da sua aplicação, o Controller da MVC acaba acumulando muitas responsabilidades.

Essa limitação não é só da MVC e também se repete na MVVM e outras arquiteturas.

Conforme a aplicação evolui e novas funcionalidades são adicionadas, as camadas tendem a ficar sobrecarregadas.

Isso não só prejudica a estética do código, como também causa impactos práticos, pois o software fica tão “engessado” que, muitas vezes, é necessário utilizar frameworks ou bibliotecas para conseguir fazer alguma modificação.

Mas existe uma alternativa para você minimizar essas limitações de arquitetura: a Clean Swift.

O que é Clean Swift?

A Clean Swift é uma adaptação da Clean Architecture (em português, “arquitetura limpa”) para o desenvolvimento de aplicativos iOS.

A Clean Architecture, proposta por “uncle Bob”, é uma arquitetura baseada em camadas, que divide o software em partes diferentes, sendo que cada camada tem uma responsabilidade e/ou funcionalidade específica.

Neste artigo, não vamos explorar diretamente a arquitetura limpa. Se você quiser conhecer um pouco mais, veja esse conteúdo.

Embora muitas arquiteturas se inspirem na Clean Architecture, cada uma pode utilizar diferentes tecnologias e ter suas próprias maneiras de organizar as camadas, o que as torna únicas.

Os princípios que orientam a Clean Architecture também moldam a Clean Swift, mas aqui, o foco é nas necessidades e particularidades do ambiente iOS.

Voltando na analogia de um quarto bagunçado, em vez de deixar as roupas desorganizadas, é mais interessante guardá-las dentro de gavetas: uma para camisas, outra para calças.

Assim, fica mais fácil encontrar uma roupa ou fazer modificações no seu guarda-roupa, por exemplo.

Agora, vamos entender as “gavetas”, ou seja, as camadas do Clean Swift.

Camadas do Clean Swift

Cada arquitetura tem sua organização de camadas e, na Clean Swift, não é diferente. Temos a seguinte divisão:

  • ViewController: exibe dados e captura interações da pessoa usuária, e não deve conter lógica de negócios;
  • Interactor: processa dados e toma decisões baseadas nas ações da pessoa usuária ou em eventos do sistema;
  • Presenter: formata dados do Interactor para a View;
  • Router: gerencia a navegação entre telas;
  • Worker: realiza acesso a recursos, como chamadas de rede ou manipulação de dados.

Para entender melhor essa estrutura, vamos analisar um software de sistema de saque bancário:

class BankingService {
    func withdrawAmount(amount: Double, completion: @escaping (Bool) -> Void) {
        // Verificação de saldo
        if checkBalance() >= amount {
            // Processamento do saque
            processWithdrawal(amount: amount)
            // Registro do saque no banco de dados
            Database.saveTransaction(amount)
            completion(true)
        } else {
            completion(false)
        }
    }

    private func checkBalance() -> Double {
        // Verifica saldo (implementação simplificada)
        return 1000.0
    }

    private func processWithdrawal(amount: Double) {
        // Lógica de saque
    }
}

Observe que o BankingService realiza várias tarefas que deveriam estar em camadas separadas: verificação de saldo, processamento do saque e registro no banco de dados.

Isso pode trazer problemas futuros, e a arquitetura Clean Swift pode nos ajudar a evitá-los!

ViewController

A ViewController é responsável por capturar interações da pessoa usuária e repassar solicitações ao Interactor, além de exibir resultados do Presenter.

// WithdrawViewController.swift

class WithdrawViewController: UIViewController {
    var interactor: WithdrawInteractorProtocol?    
    func withdraw(amount: Double) {
        let request = Withdraw.Request(amount: amount)
        interactor?.withdraw(request: request)
    }

    func displayWithdrawResult(viewModel: Withdraw.ViewModel) {
        if viewModel.success {
            showConfirmation("Saque realizado com sucesso.")
        } else {
            showError(viewModel.errorMessage)
        }
    }

    private func showConfirmation(_ message: String) {
        // Exibe uma mensagem de confirmação
    }

    private func showError(_ message: String) {
        // Exibe uma mensagem de erro
    }
}

A WithdrawViewController agora apenas envia dados para o Interactor e exibe resultados do Presenter.

O método withdraw cria um Withdraw.Request e o envia ao Interactor, enquanto displayWithdrawResult exibe os resultados.

Interactor

O Interactor processa a lógica de negócios, recebendo solicitações da View e solicitando ao Worker a execução de tarefas específicas.

Veja o código a seguir:


// WithdrawInteractor.swift

class WithdrawInteractor: WithdrawInteractorProtocol {
    var presenter: WithdrawPresenterProtocol?
    var worker: WithdrawWorkerProtocol = WithdrawWorker()

    func withdraw(request: Withdraw.Request) {
        guard request.amount > 0 else {
            presenter?.presentWithdrawResult(response: Withdraw.Response(success: false, errorMessage: "O valor do saque deve ser maior que zero."))
            return
        }

        worker.executeWithdraw(amount: request.amount) { success in
            let response = Withdraw.Response(success: success, errorMessage: success ? nil : "Saldo insuficiente.")
            self.presenter?.presentWithdrawResult(response: response)
        }
    }
}

O WithdrawInteractor valida o valor do saque e, se for válido, solicita ao WithdrawWorker a execução. Após receber o resultado do Worker, ele cria um Withdraw.Response e o envia ao Presenter.

Worker

O Worker implementa ferramentas, bibliotecas e realiza tarefas assíncronas, como chamadas de rede e armazenamento, permitindo que o Interactor se concentre apenas na lógica de negócios.

// WithdrawWorker.swift

class WithdrawWorker: WithdrawWorkerProtocol {
    func executeWithdraw(amount: Double, completion: @escaping (Bool) -> Void) {
        // Simula a verificação de saldo e autorização do saque
        let currentBalance = 1000.0
        if amount <= currentBalance {
            // Realiza o saque
            completion(true)
        } else {
            // Saldo insuficiente
            completion(false)
        }
    }
}

O WithdrawWorker verifica o saldo e autoriza o saque, retornando true ou false com base na disponibilidade.

Presenter

O Presenter prepara os dados recebidos do Interactor para exibição na View.

Vejamos isso na prática:

// WithdrawPresenter.swift

class WithdrawPresenter: WithdrawPresenterProtocol {
    weak var viewController: WithdrawViewController?

    func presentWithdrawResult(response: Withdraw.Response) {
        let viewModel = Withdraw.ViewModel(success: response.success, errorMessage: response.errorMessage)
        viewController?.displayWithdrawResult(viewModel: viewModel)
    }
}

O WithdrawPresenter transforma o Withdraw.Response do Interactor em um Withdraw.ViewModel, que é mais adequado para a exibição na View.

Router

O Router é responsável por gerenciar todas as opções de navegação que o ViewController pode usar.

Veja o exemplo abaixo:

// WithdrawRouter.swift

class WithdrawRouter {
    weak var viewController: WithdrawViewController?

    func navigateToConfirmation() {
        // Lógica de navegação para a tela de confirmação
    }
}

O WithdrawRouter define e executa a lógica de navegação, delegando essa responsabilidade à WithdrawViewController.

Ufa! Vimos bastante código.

E, assim, mostramos para você como se estruturam as camadas e o código de um projeto com os princípios do Clean Swift.

Qual a diferença entre Clean Swift e a arquitetura VIP?

Na internet, você encontrará artigos que dizem que o Clean Swift é a mesma coisa que a arquitetura VIP.

Algumas pessoas, no entanto, defendem que a arquitetura VIP é uma variação do Clean Swift.

Portanto, é uma questão aberta ao debate. Qual a sua opinião?

De qualquer forma, caso queira conhecer mais sobre a arquitetura VIP, consulte este curso.

Como usar o Clean Swift de forma simples

Agora que você conhece um pouco sobre Clean Swift, as camadas que compõem essa arquitetura e as vantagens de usá-la, pode surgir uma dúvida: "Será que não vai ser muito trabalhoso e complexo criar tantas camadas para cada tela ou componente?"

Sim! Se você escrever todo o código manualmente, pode dar bastante trabalho.

A boa notícia é que existe uma solução que facilita esse processo: os templates!

Os templates são modelos prontos que automatizam a criação das camadas de uma arquitetura. Eles economizam tempo, pois fornecem uma estrutura de código pronta, evitando a necessidade de escrever tudo do zero.

E, claro, existem vários templates disponíveis que ajudam a implementar o Clean Swift na sua aplicação.

Conclusão

Se você quer elevar o nível das suas aplicações, planejar o desenvolvimento utilizando Clean Swift é uma ótima escolha.

Neste artigo descobrimos a importância de uma arquitetura de software, o que é o Clean Swift e exemplo de sua aplicação em um projeto da vida real.

Onde estudar iOS

Você pode estudar mais sobre iOS aqui na Alura!

Confira as nossas formações:

Mikael Diniz
Mikael Diniz

Atualmente estou cursando Ciência da Computação na UFMA e, sou apaixonado por programação, games, matemática e basquete.

Veja outros artigos sobre Front-end