Daniel Bonates, problem solver & iOS guy at Peixe Urbano. Minhas paixões digitais são design e desenvovimento de apps para a plataforma Apple. Vivo dessas coisas desde 1998 e meu primeiro app foi para o MacOS 9, em 1991, um player de audio ;) Fiquei desviado produzindo backend e frontend para web, até que a chegada do iPhone me salvou mais uma vez, quando então comprei um Mac e voltei a ser feliz, hoje focado em MacOS, iOS e derivados!
Disclaimer 1
: Estarei tratando aqui do assunto Enum usado Swift.
Introdução
Sempre que falo de Enums, o faço com um sentimento de gratidão por um recurso que já fez muito por mim e pelo código que escrevo. Antes dele, era quase tudo literal, sabe…frágeis strings e constantes para todo lado! Nesse artigo enfrentarei 3 desafios: passar conteúdo útil, ser pragmático e objetivo, e testar minha capacidade de síntese escrevendo o mínimo de blábláblá possível.
Sobre a minha querida Objective-C:
mas voltando ao assunto…
Enum fundamental
Começando do começo, usamos enums para enumerar casos possíveis (ou se preferir, cases ou ainda, statements):
Considere essa estrutura:
struct Oferta {
let titulo: String
let tipo: String
}
Essa struct possui um título que provavelmente é único. Mas será que podemos dizer o mesmo do tipo? Vejamos o caso desse array:
let ofertas = [
Oferta(titulo: "Agua Mineral", tipo: "normal"),
Oferta(titulo: "Macarrão", tipo: "extra"),
Oferta(titulo: "Vassoura", tipo: "normal"),
Oferta(titulo: "Shampoo", tipo: "normal"),
Oferta(titulo: "Sabonente", tipo: "extra"),
Oferta(titulo: "Coleira", tipo: "extra"),
Oferta(titulo: "Nutella", tipo: "relampago")
]
Repetição, margem de erro, retardamento no entendimento de todos os tipos… tudo isso eu sou capaz de sofrer. Então, que tal enumerar os tipos de ofertas? Sugestão:
enum DescontoTipo {
case normal
case extra
case relampago
}
Daí nossa struct ficaria ao invés de usar String em tipo, usa nosso enum DescontoTipo:
struct Oferta {
let titulo: String
let tipo: DescontoTipo
}
daí o nosso array de ofertas fica assim:
let ofertas = [
Oferta(titulo: "Agua Mineral", tipo: .normal),
Oferta(titulo: "Macarrão", tipo: .extra),
Oferta(titulo: "Vassoura", tipo: .normal),
Oferta(titulo: "Shampoo", tipo: .normal),
Oferta(titulo: "Sabonente", tipo: .extra),
Oferta(titulo: "Coleira", tipo: .extra),
Oferta(titulo: "Nutella", tipo: .relampago)
]
Simples, efetivo e strongly typed! Sempre que formos criar uma Oferta
, o compilador não vai aceitar nada que seja diferente de DescontoTipo na propriedade tipo, prevenindo erros, entre outras vantagens que já citei.
Reforçando tipos e legibilidade
Enums também podem tornar seu código muito mais legível e flexível. Por exemplo, e se nossa Oferta
possuir um status de disponibilidade? Talvez o nosso primeiro impulso seria o de criar uma propriedade do tipo Bool:
struct Oferta {
let titulo: String
let tipo: DescontoTipo
let status: Bool
}
É ok trabalhar aqui com true/false. Vai dar conta de boa, certo?! Depende!
Fazendo isso você acaba de condenar a coitada da Oferta
a estar disponível ou não, apenas 2 estados! Quer ver? E se a turma de produto chega depois e diz que a oferta pode estar disponível, indisponível, suspensa, encerrada, etc?
Veja como o uso de um enum no lugar de Bool, vai te dar muito mais flexibilidade:
enum OfertaStatus {
case disponivel
case expirada
case suspensa
case encerrada
}
Nossa Oferta
fica assim:
struct Oferta {
let titulo: String
let tipo: DescontoTipo
let status: OfertaStatus
}
daí para criar uma uma oferta:
let oferta = Oferta(titulo: "Nutella", tipo: .normal, status: .disponivel)
nice 😊!
Acredito que até aqui, já deu pra ver que usar enums é uma obrigação se você pretende manter seu código legível e eficiente. Então vamos agora a um case bem mais comum e pragmático.
Enum na rota, um caso mais “PRO”
Diz-se que em Swift enums tem super poderes. E com razão. São value types, podem ter extensions, variaveis computadas, receber parametros, ter metodos etc. Vamos explorar algumas dessas ferramentas e mostrar como isso pode ser bom pra gente:
A partir daqui:
Nosso cenário: vamos simular que nosso app consome uma API da web, que possui várias rotas e versões.
Nossa missão: criar um enum
Router
para gerenciar todas as rotas do nosso app.
Disclaimer 2
: Sobre a ‘sacada’ que vamos abordar:
Embora a solução final que vou apresentar aqui não seja de minha autoria, acho que vale como caso real pois foi justamente num app super complexo de ecommerce que a vi implementada. Achei uma solução sensacional assim que vi, e a partir daí adotei (com pequenas modificações) em todo app que trabalho. O autor dessa sacada genial foi o Cassius Pacheco que havia deixado sua marca no app pouco antes de eu chegar na empresa. Vamos destrinchar passo a passo essa solução pra entender como ele conseguiu resolver um sistema complexo de rotas de uma API explorando todo poder de um Enum em Swift.
Mas vamos começar devagar, com uma versão bem simples da API e vamos aumentando a complexidade para ver até onde o uso de um enum pode nos ajudar.
Nossa primeira versão da API é https://bonates.com/usuarios
, que retorna um json com a lista dos usuários.
Essa é a func
que usaremos para testar a construção da nossa rota:
// essa é a nossa função que carrega dados da web
func loadData(request: URLRequest) {
URLSession.shared.dataTask(with: request) { _,_,_ in
print("loaded!")
}
}
Para a primeira versão da API, esse código dá conta de fazer o download dos dados:
let usersURL = URL(string: "https://bonates.com/usuarios")!
let request = URLRequest(url: usersURL)
// carregando a lista de usuários
loadData(request: request)
Usei force unwrap aqui! Relaxa que já vai passar!
Isso pro seu teste num Playground tá ok, mas num app, consumindo uma API de verdade, uma url só não é nossa realidade, confere?
E tem mais! Agora precisamos acessar agora uma nova rota da nossa api que retorna a lista de ofertas: https://bonates.com/ofertas
. Bastaria criar uma nova e fedorenta variavel ofertasURL
, certo? Não! Vamos criar nosso enum para começar a cuidar das rotas:
enum Router: String {
case usuarios = "https://bonates.com/usuarios"
case ofertas = "https://bonates.com/ofertas"
}
Daí basta usar nosso Router pra buscar a url da vez:
let usersURL = URL(string: Router.usuarios.rawValue)! // force unwrap de novo :boom:
Pronto, de novo nos livramos de literais e quem olhar essa linha de código vai saber que estamos recuperando uma url relacionada a usuarios. Bem mais profissional, não?! (Eu sei, mas ainda tem a exclamação do mal ali, calma, já disse)
Mas isso é muito pouco perto do que ainda podemos fazer pra esse código ficar bom. E tem mais uma novidade: nossa api retorna também ofertas favoritas do usuário logado. O link é esse: https://bonates.com/ofertas/favoritas
Enums aceitam variáveis computadas, então, ao invés de criar outro case favoritas
e retornar toda a url inteira, podemos torna-la mais dinâmica gerando-a a partir de seus componentes. Isso fica assim:
enum Router {
case usuarios
case ofertas
case favoritas
// host comum a todos
var basePath: String {
return "https://bonates.com"
}
// o path que varia de acordo com o case
var path: String {
switch self {
case .usuarios: return "usuarios"
case .ofertas: return "ofertas"
case .favoritas: return "ofertas/favoritas"
}
}
// o request que nos interessa
var request:URLRequest? {
let baseUrlPath = "\(basePath)/\(path)"
let baseURL = URLComponents(string: baseUrlPath)
guard
let url = baseURL?.url else {
print("Impossível iniciar request com essa url.")
return nil
}
let request = URLRequest(url: url)
return request
}
}
Nosso Router
agora nos retorna um request pronto, bastando pra isso dizer qual request queremos, inclusive com a ajuda do auto complete para os mais preguiçosos, me included o/
So far, so good ;) Olha como agora um simples if-let resolve nossos problemas de request válido e uso de force unwrap. No código a seguir, o request vai me retornar https://bonates.com/ofertas/favoritas
lindamente, safe and sound!
if let request = Router.favoritas.request {
loadData(request: request)
}
Passando parametros e query strings
Nossa api agora tem um recurso que lista apenas os usuários ativos: https://bonates.com/usuarios?status=ativos
. Daí, evoluindo nosso enum:
1 - mudamos o case .usuarios
para aceitar parametros:
case usuarios([String:Any])
2 - adicionamos uma variável computada:
var parametros: [String: Any] {
switch self {
case .usuarios(let urlParams):
return urlParams
default:
return [:]
}
}
3 - E fazemos uso dela na geração do request:
...
baseURL?.queryItems = parametros.map{ qParamter in
return URLQueryItem(
name: qParamter.key,
value: "\(qParamter.value)"
)
}
...
Agora qualquer quantidade de parametros passados como String:Any
vai ser aceito e entrar como query string:
// request vai retornar:
// https://bonates.com/usuarios?cidade=rio-de-janeiro&status=ativo
let parametros = ["status":"ativo", "cidade": "rio-de-janeiro"]
if let usuariosAtivosReq = Router.usuarios(parametros).request {
loadData(request: request)
}
Metodo HTTP
Perto do que já fizemos, incluir a capacidade de atribuir um método http de acordo com o caso acaba sendo trivial. Basta adicionar uma variável no Router com essa finalidade:
var metodo: String {
switch self {
default:
return "GET"
}
}
Só deixei o GET
implementado como case padrão. Incrementar um POST, DELETE, etc
é com você!
O Router até aqui ficou assim:
enum Router {
case usuarios([String:Any])
case ofertas
case favoritas
var basePath: String {
return "https://bonates.com"
}
var path: String {
switch self {
case .usuarios: return "usuarios"
case .ofertas: return "ofertas"
case .favoritas: return "ofertas/favoritas"
}
}
var parametros: [String: Any] {
switch self {
case .usuarios(let urlParams):
return urlParams
default:
return [:]
}
}
var metodo: String {
switch self {
default:
return "GET"
}
}
var request:URLRequest? {
let baseUrlPath = "\(basePath)/\(path)"
var baseURL = URLComponents(string: baseUrlPath)
baseURL?.queryItems = parametros.map{ qParamter in
return URLQueryItem(name: qParamter.key, value: "\(qParamter.value)")
}
guard
let url = baseURL?.url else {
print("Impossível iniciar request com essa url.")
return nil
}
let request = URLRequest(url: url)
return request
}
}
Pronto! Cobrimos um caso real explorando os recursos de enumeração disponíveis em Swift para criarmos um sistema de rotas fácil de escalar e fazer manutenção. Tem mais possibilidades, mas basicamente elas seriam uma combinação do que já fizemos aqui ;)
Enums + Generics - o poder não tem limites!
E pra fechar, você sabia que uma variável optional na real implementa o enum Optional da biblioteca padrão do Swift?
// Optional simplificado
enum Optional<T> {
case none
case some(T)
}
ou seja, quando definimos uma variável como optional, basicamente estamos fazendo isso:
let variavel1: Int? = nil // nil
let variavel2: Optional<Int> = .none // nil
let variavel3: Int? = 42 // 42
let variavel4: Optional<Int> = .some(42) //42
Viu só?! Enum está no coração do Swift 😊!
Usando essa mesma idéia, podemos criar por exemplo um recurso muito útil para tratar erro e sucesso em requests:
enum RequestResult<T> {
case successo(T)
case erro(Error)
}
A dica aqui é sua rotina retornar um enum RequestResult, verificando o resultado da operação:
WebService.json(from: request) { result in
switch result {
case .erro(let erro):
// tratar o erro
case .successo(let json):
// usar os dados retornados
}
}
Fica fácil, né?!
Considerações finais
Bem, esses são os meus argumentos pra te convencer de que enumerar seus dados usando os super poderes da nossa linguagem do coração, vai deixar seu código lindo, seus coleguinhas vão curtir quando precisarem dar manutenção e provavelmente o Crashlytics vai dar uma relaxada porque você não tipou errado! Espero ter contribuído com algumas idéias e uma inspiração a mais para tornar seu código mais seguro, organizado e fácil de ler.
Dúvidas, comentários, whatever, pode tacar por aqui, me parar na rua, ou me procurar lá no Slack da comunidade iOS, onde estou sempre presente, ainda que apenas invisível só acompanhando as tretas!
Abraços e até logo!