O URLNavigator é uma biblioteca de roteamento de URLs elegante para Swift, oferecendo funcionalidades robustas que simplificam o desenvolvimento de aplicações iOS. Neste artigo, exploramos dois recursos avançados: conversores de valor personalizados e padrões de rota complexos, que permitem construir sistemas de navegação mais flexíveis e potentes.
A Necessidade de Conversores de Valor Personalizados
O URLNavigator inclui conversores embtuidos como string, int, float, uuid e path. Contudo, cenários reais frequentemente exigem lógica de validação mais complexa, como verificar formatos específicos (ex.: regiões de AWS, endereços de e-mail) ou aceitar apenas valores de uma enumeração personalizada. Conversores de valor personalizados concedem controle total sobre a análise dos parâmetros da URL.
Criando um Conversor Personalizado
Um convesror de valor é deifnido como um closure que recebe um array de componentes do caminho e um índice, retornando o valor convertido ou nil para indicar falta de correspondência. Vejamos um exemplo que valida códigos de região:
let regioesPermitidas = Set(["us-west-1", "ap-northeast-2", "eu-west-3"])
navigator.matcher.valueConverters["region"] = { componentes, idx in
guard idx < componentes.count else { return nil }
let candidato = componentes[idx]
return regioesPermitidas.contains(candidato) ? candidato : nil
}
A definição de tipo para conversores segue o padrão ([String], Int) -> Any?, conforme observado em URLMatcher.swift.
Aplicando o Conversor em uma Rota
Após a definição, utilize o conversor em um padrão de URL:
navigator.register("myapp://region/<_>") { url, valores, contexto in
guard let regiao = valores["region"] as? String else { return nil }
return RegionViewController(region: regiao)
}</_>
As URLs myapp://region/us-west-1 e myapp://region/eu-west-3 corresponderão, enquanto myapp://region/ca-central-1 não.
Exemplos de Conversores Mais Elaborados
1. Validação de E-mail
navigator.matcher.valueConverters["email"] = { componentes, idx in
let candidato = componentes[idx]
let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let teste = NSPredicate(format:"SELF MATCHES %@", regex)
return teste.evaluate(with: candidato) ? candidato : nil
}
2. Conversor de Data
navigator.matcher.valueConverters["date"] = { componentes, idx in
let texto = componentes[idx]
let formatador = DateFormatter()
formatador.dateFormat = "yyyy-MM-dd"
return formatador.date(from: texto)
}
Construindo Padrões de Rota Complexos
Combinando Múltiplos Conversores
É possível utilizar diferentes conversores em uma única rota:
navigator.register("myapp://products/<cat_id>/<slug>") { url, valores, ctx in
guard let idCategoria = valores["cat_id"] as? Int,
let slug = valores["slug"] as? String else { return nil }
return ProductViewController(categoryId: idCategoria, slug: slug)
}</slug></cat_id>
Lidando com Profundidade Dinâmica de Caminhos
O conversor path captura o restante do caminho da URL:
navigator.register("myapp://docs/<doc_path>") { url, valores, ctx in
guard let caminho = valores["doc_path"] as? String else { return nil }
return DocumentationViewController(path: caminho)
}</doc_path>
Esta rota corresponderá a URLs como myapp://docs/guia/avancado/uso.
Combinando Parâmetros de Caminho e de Consulta
navigator.register("myapp://search/<query>") { url, valores, ctx in
guard let consulta = valores["query"] as? String else { return nil }
let parametros = url.urlValue?.queryParameters ?? [:]
let pagina = parametros["page"] ?? "1"
let ordenacao = parametros["sort"] ?? "relevance"
return SearchViewController(query: consulta, page: pagina, sort: ordenacao)
}</query>
Exemplo de uso: navigator.push("myapp://search/swift?page=2&sort=date")
Recomendações de Implementação
1. Mapa de Navegação Centralizado
Reúna todas as definições de rotas em um único local:
struct MapaNavegacao {
static func inicializar(nav: NavigatorProtocol) {
nav.register("myapp://user/<id>") { url, valores, ctx in
guard let userId = valores["id"] as? Int else { return nil }
return UserViewController(userId: userId)
}
configurarConversores(nav: nav)
}
private static func configurarConversores(nav: NavigatorProtocol) {
nav.matcher.valueConverters["region"] = { componentes, idx in
let validas = ["us-west-1", "ap-northeast-2", "eu-west-3"]
return validas.contains(componentes[idx]) ? componentes[idx] : nil
}
}
}</id>
2. Inicialização no AppDelegate
class AppDelegate: UIResponder, UIApplicationDelegate {
let navigator = Navigator()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
MapaNavegacao.inicializar(nav: navigator)
if let urlInicial = launchOptions?[.url] as? URL {
processarURL(urlInicial)
}
return true
}
}
3. Tratamento de Erros para URLs Desconhecidas
extension Navigator {
func lidarComURLDesconhecida(_ url: URLConvertible, contexto: Any? = nil) -> Bool {
print("URL sem correspondência: \(url.urlStringValue)")
if let vcAtual = UIViewController.topMost {
let alerta = UIAlertController(title: "Página não encontrada",
message: "Não foi possível abrir: \(url.urlStringValue)",
preferredStyle: .alert)
alerta.addAction(UIAlertAction(title: "OK", style: .default))
vcAtual.present(alerta, animated: true)
}
return false
}
}
Otimizando a Performance
Atraso na Inicialização de Conversores
navigator.matcher.valueConverters["complexo"] = { componentes, idx in
return validarComplexo(componentes[idx])
}
Cache de Resultados de Conversão
var cacheValidacao = [String: Bool]()
navigator.matcher.valueConverters["cached"] = { componentes, idx in
let valor = componentes[idx]
if let resultado = cacheValidacao[valor] {
return resultado ? valor : nil
}
let valido = validarCustoAlto(valor)
cacheValidacao[valor] = valido
return valido ? valor : nil
}
Testando Conversores Personalizados
class TestesConversores: XCTestCase {
func testConversorRegiao() {
let nav = Navigator()
nav.matcher.valueConverters["region"] = { componentes, idx in
let permitidas = ["us-west-1", "eu-west-3"]
return permitidas.contains(componentes[idx]) ? componentes[idx] : nil
}
// Testes positivos
XCTAssertNotNil(nav.matcher.valueConverters["region"]?(["us-west-1"], 0))
XCTAssertNotNil(nav.matcher.valueConverters["region"]?(["eu-west-3"], 0))
// Testes negativos
XCTAssertNil(nav.matcher.valueConverters["region"]?(["invalida"], 0))
}
}