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 dois blocos de texto, no qual o bloco esquerdo tem os dizeres:

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