Técnicas Avançadas do URLNavigator: Conversores de Valor Personalizados e Padrões de Rota Complexos

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))
    }
}

Tags: swift ios URLNavigator roteamento URL

Publicado em 6-4 16:49 por Thomas