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.
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: