Como configurar o roteamento do WKWebView para suportar Universal Links no iOS

opoinstall
2026-10-07
5 min read

Como posso habilitar Universal Links dentro de um WKWebView no iOS? Os Universal Links já podem ser resolvidos a partir de links elegíveis dentro de um WKWebView. Implementar o WKNavigationDelegate permite que o aplicativo hospedeiro personalize as políticas de roteamento — interceptando destinos próprios do aplicativo, controlando handoffs externos via decidePolicyForNavigationAction e reforçando a segurança no nível de frame.

Na arquitetura de aplicativos iOS, a interceptação de navegação em WKWebView permite que o app personalize políticas de roteamento para Universal Links, esquemas de URL personalizados e destinos web. Ao avaliar metadados de requisição de navegação dentro de um WKNavigationDelegate, o aplicativo pode rotear destinos internos, delegar alvos externos para manipuladores do sistema e aplicar políticas de segurança de frame.

Termo Definição Entidade Relacionada Intenção de Busca
WebView Um componente de visualização baseado em WebKit que renderiza conteúdo web interativo dentro de apps iOS. SDK iOS Informativa / Comercial
Universal Links Um mecanismo HTTPS padrão que vincula domínios verificados a visualizações de aplicativos nativos iOS. Roteamento de Deep Link Técnica / Informativa
Custom URL Scheme Um esquema de URI definido pelo app usado para rotear URLs para um aplicativo nativo. Deep Linking Móvel Informativa

Como o WKWebView e o roteamento de Universal Links interagem no iOS

O WKNavigationDelegate decide se o WebKit carrega, roteia internamente, transfere para o sistema ou cancela a navegação.

Ciclos de vida de navegação do WebKit e políticas de roteamento do aplicativo

A Apple implementa Universal Links como um mecanismo de roteamento em nível de sistema suportado no Safari e em ambientes WKWebView. Quando um usuário toca em um link elegível dentro de um WKWebView, a plataforma pode resolver associações de domínio e rotear a execução de acordo com as políticas do sistema operacional.

Embora Universal Links reconhecidos pelo sistema possam transferir a execução para manipuladores nativos, ambientes de navegação embutidos frequentemente exigem uma lógica de roteamento específica do aplicativo. Por exemplo, quando um link aponta para o domínio do próprio aplicativo, desenvolvedores preferem navegar diretamente através de view controllers nativos sem disparar o relançamento do app. Implementar WKNavigationDelegate fornece controle granular sobre a avaliação de links, permitindo que equipes apliquem allowlists personalizadas e roteiem destinos internos de forma previsível.

A barreira na experiência do usuário: quando a navegação in-app aprisiona usuários em loops de redirecionamento

WebViews são frequentemente usados para hospedar microsites promocionais, centros de ajuda, catálogos de parceiros e landing pages dentro de apps iOS. Quando uma página web in-app inclui links destinados a levar usuários para outras seções do próprio app (ex: botões “Visualizar no App”) ou para apps de parceiros, a navegação padrão pode levar a uma renderização redundante:

  • Renderização Web Redundante: Em vez de renderizar view controllers nativos, o usuário pode ser apresentado a versões web responsivas de páginas in-app, exigindo autenticações repetidas e degradando a consistência visual.
  • Aprisionamento Web: Usuários podem ficar presos em pilhas de navegação web sem formas intuitivas de retornar às interfaces nativas principais.
  • Falhas na transição entre apps: Tocar em links que apontam para serviços de terceiros (como apps de navegação, diálogos de compartilhamento social ou gateways de pagamento) exige uma delegação explícita caso esses serviços dependam de esquemas de URL personalizados.

Comparando WKWebView com SFSafariViewController para Handoffs Web-para-App

Ao arquitetar a navegação web in-app no iOS, equipes de engenharia devem escolher entre WKWebView e SFSafariViewController:

  • SFSafariViewController: Oferece uma interface de navegação do Safari gerenciada pelo sistema, com recursos como preenchimento automático e bloqueio de conteúdo. O aplicativo hospedeiro não pode inspecionar a atividade de navegação ou dados do site, e a personalização de UI é limitada.
  • WKWebView: Um componente de visualização embutido hospedado dentro do processo de UI do app, executando conteúdo web em processos separados do WebKit. Ele permite personalização profunda de UI, pontes de JavaScript e integração de layout personalizado, exigindo a implementação explícita de WKNavigationDelegate para roteamento.

Como o decidePolicyForNavigationAction intercepta o roteamento do WebKit

O pipeline de política de navegação: entendendo WKNavigationAction, request e decisionHandler

Para controlar o fluxo de navegação dentro de um WKWebView, desenvolvedores atribuem um delegate personalizado que segue a Documentação da Apple sobre WKNavigationDelegate. O ponto principal de interceptação é o método delegado:

func webView(
    _ webView: WKWebView,
    decidePolicyFor navigationAction: WKNavigationAction,
    decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
)

Ações de navegação disparadas por interações do usuário, redirecionamentos programáticos ou submissões de formulários passam por este método. O objeto WKNavigationAction fornece metadados chave:

  • navigationAction.request.url: A URL de destino solicitada.
  • navigationAction.navigationType: O tipo de gatilho (.linkActivated, .other, .formSubmitted).
  • navigationAction.sourceFrame: Informações sobre o frame que iniciou a requisição.
  • navigationAction.targetFrame: Informações sobre o frame de destino.

O decisionHandler é um closure de conclusão que informa ao WebKit se deve permitir ou cancelar a navegação.

Quando retornar .allow vs .cancel: Controlando o ciclo de vida do WebKit

A WKNavigationActionPolicy passada ao decisionHandler controla o comportamento do WebKit:

  • .allow: Informa ao WebKit para prosseguir com a navegação solicitada dentro da web view.
  • .cancel: Instrui o WebKit a cancelar a navegação. Esta política é executada quando o app intercepta um esquema personalizado, roteia um Universal Link internamente ou delega um alvo externo para UIApplication.shared.open().

O decisionHandler deve ser invocado exatamente uma vez por ação de navegação.

Avaliando tipos de navegação: Distinguindo cliques (.linkActivated) de redirecionamentos automáticos

WKNavigationAction.navigationType permite distinguir interações explícitas do usuário de scripts automatizados:

  • .linkActivated: O usuário tocou fisicamente em uma tag de âncora (<a href="...">).
  • .other: Representa navegações programáticas, como atualizações de window.location.href, meta refreshes ou chamadas iniciais de webView.load().
  • .formSubmitted / .formResubmitted: Representa submissões POST ou GET.

Avaliar o navigationType permite que o app aplique uma política de link explícito em nível de aplicação. Para invocações de esquemas personalizados externos, exigir .linkActivated ajuda a evitar que scripts em segundo plano disparem lançamentos de apps externos sem interação.

Manipulando políticas de decisão assíncronas sem Retain Cycles

Quando a validação de rota requer consultas a caches ou validadores de segurança:

  1. Garanta que o decisionHandler seja executado em todos os caminhos de execução, incluindo condições de erro.
  2. Use referências fracas ([weak self]) dentro de closures de escape para prevenir retain cycles entre o WKWebView, seu delegate e o UIViewController pai.

Diferenciando domínios associados do app de Universal Links externos

Universal Links pertencentes ao app devem ser validados e roteados internamente a partir do WKWebView.

Gerenciando destinos próprios vs. delegação de link no nível do sistema

Para rotas próprias do aplicativo, evite tentar reentrar no mesmo app através de uma busca de Universal Link redundante. Manipule destinos próprios diretamente através do roteador interno do aplicativo.

Essa separação arquitetural garante uma navegação fluida:

  • Domínios Associados do App: Se o host da URL corresponder ao domínio associado do app (app.example.com), cancele a navegação na web view (decisionHandler(.cancel)), valide o caminho da rota e passe os parâmetros diretamente para o roteador interno do aplicativo.
  • Aplicativos Externos: Se a URL aponta para um parceiro externo ou esquema personalizado permitido, aplique uma barreira de link explícito (navigationType == .linkActivated), cancele a navegação e encaminhe a requisição para UIApplication.shared.open(url).

Arquitetando validação de rotas internas: Extraindo caminhos e parâmetros via AppRouteValidator

Quando uma URL recebida corresponde ao domínio associado, ela deve passar por um validador rigoroso antes de disparar transições de view controller.

O modelo AppRouteValidator:

  • Valida o caminho da URL contra uma allowlist de rotas internas suportadas (ex: /open/, /product/, /promo/, /checkout/).
  • Extrai parâmetros de query (ex: id, promo, utm_source).
  • Reforça restrições de conjuntos de caracteres, limites de tamanho e rejeição de chaves duplicadas.

Manipulando Universal Links de terceiros via delegação ao UIApplication

Universal Links externos podem tentar handoff nativo e, se falhar, retornar para a web.

Quando uma página web dentro de um WKWebView aponta para serviços externos, o aplicativo hospedeiro pode delegar o roteamento para o sistema iOS:

let options: [UIApplication.OpenExternalURLOptionsKey: Any] = [
    .universalLinksOnly: true
]
UIApplication.shared.open(url, options: options) { success in
    if !success {
        // Fallback de política: carregar destino web caso nenhum app nativo manipule o Universal Link
    }
}

Gerenciando fallbacks de esquemas personalizados (myapp://) junto com HTTPS

Embora Universal Links HTTPS representem o deep linking padrão, esquemas personalizados (myapp:// ou partnerapp://) continuam comuns em campanhas promocionais.

Em uma implementação unificada de WKNavigationDelegate:

  • Esquemas não-HTTP/HTTPS são inspecionados primeiro. Se o esquema coincidir com um protocolo permitido e satisfizer a política de link explícito (navigationType == .linkActivated), o delegate verifica o host e os parâmetros antes de despachar para UIApplication.shared.open().
  • Esquemas não reconhecidos ou invocações em segundo plano não solicitadas são cancelados imediatamente.
[Usuário Interage com Link no iOS WKWebView]
                       │
                       ▼
[WKNavigationDelegate: decidePolicyForNavigationAction]
                       │
         ┌─────────────┴─────────────┐
         ▼                           ▼
[!action.sourceFrame.isMainFrame] [action.sourceFrame.isMainFrame]
         │                           │
         ▼                           ▼
[Barreira de Segurança de Subframe] [Inspecionar Esquema & Host de Destino]
├─ HTTP(S) -> .allow                 │
└─ Não-Web -> .cancel    ┌───────────┼───────────┐
   (Suprimir Subframe)   ▼           ▼           ▼
                   [Domínio Host] [Web Externa] [Esquema Customizado]
                         │           │           │
                         ▼           ▼           ▼
                   [AppRoute]    [Verificar Link] [Verificar Link]
                   ├─ Válido ->   ├─ Parceiro->   ├─ Válido & Clique->
                   │  Interno     │  Abrir App    │  Abrir App
                   └─ Inválido->  └─ Web ->      └─ Inválido/Auto->
                      .cancel       .allow         .cancel

Como reforçar a segurança do Main Frame e prevenir Iframe Hijacking

O frame de origem e o contexto de destino do WKNavigationAction determinam a política de navegação segura.

Tratando a navegação web embutida como entrada não confiável: Padrões OWASP

De acordo com o Guia da OWASP para Deep Links Inseguros, todas as URLs e cargas de parâmetros processadas por manipuladores de navegação móvel devem ser tratadas como entrada externa e não confiável.

Páginas web renderizadas dentro de um WKWebView podem carregar scripts de terceiros, banners publicitários ou conteúdo gerado pelo usuário. Se um delegate de navegação encaminhar URLs arbitrárias para view controllers nativos sem validação, parâmetros inesperados podem atingir rotas internas sensíveis.

Isolando navegações do Main Frame de Iframes e novos alvos de janela

De acordo com a Documentação da Apple sobre WKNavigationAction, avaliar a segurança do frame exige verificar o frame de origem:

  • sourceFrame.isMainFrame == true: A navegação foi iniciada diretamente pelo frame do documento principal.
  • sourceFrame.isMainFrame == false: A navegação foi iniciada por um subframe ou iframe embutido.
  • targetFrame == nil: A navegação solicita um novo alvo de janela (ex: âncora com target="_blank").

Para prevenir o Iframe Hijacking — onde um iframe tenta lançar apps externos ou disparar transições nativas em segundo plano — o delegate deve avaliar sourceFrame.isMainFrame. Se a origem for um iframe, permita a navegação HTTP/HTTPS padrão, mas bloqueie esquemas personalizados ou handoffs nativos.

Prevenindo invocações maliciosas de protocolo e flooding de esquemas em segundo plano

Verificações de frame impedem que subframes disparem invocações de esquema externas sem interação:

if !navigationAction.sourceFrame.isMainFrame {
    let scheme = url.scheme?.lowercased() ?? ""
    if scheme == "http" || scheme == "https" {
        decisionHandler(.allow) // Permitir navegação HTTP(S) padrão em subframes
    } else {
        decisionHandler(.cancel) // Suprimir esquemas não-web vindos de subframes
    }
    return
}

Reforçando allowlists de caminhos e parâmetros no roteamento do cliente

Ambas as URLs de domínios associados internos e esquemas customizados devem passar por modelos validadores antes da execução:

  • Allowlisting de Prefixos de Caminho: Reforce prefixos de rota aprovados, rejeitando caminhos arbitrários.
  • Filtragem de Chaves de Query: Descarte chaves de consulta inesperadas para prevenir poluição de parâmetros.
  • Restrições de tipo e tamanho: Restrinja valores a conjuntos de caracteres alfanuméricos e imponha limites de tamanho (≤64\le 64 caracteres).

Implementação de WKNavigationDelegate em Swift para produção

Estruturando o CustomWebViewController e a arquitetura de delegate em Swift

Um controlador WKWebView de produção coordena configuração web, avaliação de políticas, roteamento interno e delegação externa. A implementação encapsula regras de validação dentro de classes dedicadas (AppRouteValidator e CustomSchemeValidator).

Implementando modelos AppRouteValidator e CustomSchemeValidator

Os modelos validadores impõem uma segurança rigorosa de fail-closed:

  • AppRouteValidator valida domínios associados internos, verificando prefixos de caminho e higienizando parâmetros de query.
  • CustomSchemeValidator verifica esquemas personalizados autorizados, hosts permitidos e higieniza valores de query.

O OpoInstall pode ser integrado junto a uma camada de roteamento própria do aplicativo para atribuição e recuperação de parâmetros adiados. Consulte a documentação de integração do SDK para guias abrangentes.

A implementação técnica abaixo demonstra como configurar um WKNavigationDelegate seguro em Swift:

// iOS: CustomWebViewController com WKNavigationDelegate rigoroso e Segurança de Frame
import UIKit
import WebKit

struct ValidatedAppRoute {
    let path: String
    let queryParams: [String: String]
}

// 1. Validador para Domínio Associado do App (Rotas Internas)
class AppRouteValidator {
    private static let allowedPrefixes = ["/open/", "/product/", "/promo/", "/checkout/"]
    private static let allowedQueryKeys = Set(["target", "id", "promo", "utm_source"])

    static func validate(url: URL) -> ValidatedAppRoute? {
        let path = url.path
        guard allowedPrefixes.contains(where: { path.hasPrefix($0) }) else {
            return nil
        }

        var sanitizedParams: [String: String] = [:]
        var seenKeys = Set<String>()

        if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
           let queryItems = components.queryItems {
            let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
            
            for item in queryItems {
                guard allowedQueryKeys.contains(item.name), !seenKeys.contains(item.name) else { return nil }
                seenKeys.insert(item.name)

                let value = item.value ?? ""
                if value.count <= 64 && value.rangeOfCharacter(from: validChars.inverted) == nil {
                    sanitizedParams[item.name] = value
                } else {
                    return nil
                }
            }
        }

        return ValidatedAppRoute(path: path, queryParams: sanitizedParams)
    }
}

// 2. Validador para Esquemas Customizados Externos (myapp://)
class CustomSchemeValidator {
    private static let allowedSchemes = Set(["myapp"])
    private static let allowedHosts = Set(["open", "product", "event"])
    private static let allowedPathPrefixes = ["/detail/", "/view/", "/main/"]
    private static let allowedQueryKeys = Set(["target", "id", "promo", "utm_source"])

    static func validate(url: URL) -> URL? {
        guard let scheme = url.scheme?.lowercased(), allowedSchemes.contains(scheme) else {
            return nil
        }
        guard let host = url.host?.lowercased(), allowedHosts.contains(host) else {
            return nil
        }

        let path = url.path
        if !path.isEmpty && !allowedPathPrefixes.contains(where: { path.hasPrefix($0) }) {
            return nil
        }

        var seenKeys = Set<String>()
        if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
           let queryItems = components.queryItems {
            let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
            for item in queryItems {
                guard allowedQueryKeys.contains(item.name), !seenKeys.contains(item.name) else { return nil }
                seenKeys.insert(item.name)

                let value = item.value ?? ""
                if value.count > 64 || value.rangeOfCharacter(from: validChars.inverted) != nil {
                    return nil
                }
            }
        }

        return url
    }
}

// 3. UIViewController que hospeda o WKWebView com Interceptação Segura
class CustomWebViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!
    private let hostAssociatedDomain = "app.example.com"
    private let allowedExternalPartnerHosts = Set(["partner.example.com"])

    override func viewDidLoad() {
        super.viewDidLoad()

        let configuration = WKWebViewConfiguration()
        webView = WKWebView(frame: view.bounds, configuration: configuration)
        webView.navigationDelegate = self
        view.addSubview(webView)
    }

    func webView(
        _ webView: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
    ) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }

        // Check 1: Enforce boundary (sourceFrame) para prevenir iframe hijacking
        if !navigationAction.sourceFrame.isMainFrame {
            let scheme = url.scheme?.lowercased() ?? ""
            if scheme == "http" || scheme == "https" {
                decisionHandler(.allow)
            } else {
                decisionHandler(.cancel)
            }
            return
        }

        let scheme = url.scheme?.lowercased() ?? ""
        let isExplicitLinkActivation = (navigationAction.navigationType == .linkActivated)

        // Check 2: Manipular Domínio Associado do App
        if scheme == "https", let host = url.host?.lowercased(), host == hostAssociatedDomain {
            if let validatedRoute = AppRouteValidator.validate(url: url) {
                AppInternalRouter.shared.navigate(to: validatedRoute)
            }
            decisionHandler(.cancel)
            return
        }

        // Check 3: Manipular Esquemas Customizados (myapp://)
        if scheme != "http" && scheme != "https" && scheme != "about" {
            if isExplicitLinkActivation, let validatedURL = CustomSchemeValidator.validate(url: url) {
                UIApplication.shared.open(validatedURL, options: [:], completionHandler: nil)
            }
            decisionHandler(.cancel)
            return
        }

        // Check 4: Destinos Externos e New-Window (target="_blank")
        if scheme == "http" || scheme == "https" {
            let host = url.host?.lowercased() ?? ""
            
            if allowedExternalPartnerHosts.contains(host) && isExplicitLinkActivation {
                let options: [UIApplication.OpenExternalURLOptionsKey: Any] = [
                    .universalLinksOnly: true
                ]
                UIApplication.shared.open(url, options: options) { [weak self] success in
                    if !success {
                        guard let self = self else { return }
                        self.webView.load(navigationAction.request)
                    }
                }
                decisionHandler(.cancel)
                return
            }

            if navigationAction.targetFrame == nil {
                webView.load(navigationAction.request)
                decisionHandler(.cancel)
                return
            }

            decisionHandler(.allow)
            return
        }

        decisionHandler(.allow)
    }
}

class AppInternalRouter {
    static let shared = AppInternalRouter()
    func navigate(to route: ValidatedAppRoute) {}
}

Execução thread-safe: Garantindo transições na Main Actor

Em modelos de concorrência Swift, callbacks de WKNavigationDelegate são isolados na main actor. Roteamento de aplicativos e transições de view controller são executados nela, mantendo a segurança de thread.

Erros e matriz diagnóstica de navegação Deep Link no WKWebView

Guia de solução de problemas

A matriz abaixo descreve modos de falha comuns ao gerenciar deep links e esquemas dentro de um WKWebView no iOS:

Sintoma Causa Raiz Versões Ponto de Verificação Remediação
Universal Link abre na Web Domínio próprio não interceptado iOS 9+ decidePolicyForNavigationAction sem tratamento Interceptar domínio host, rotear internamente, .cancel
Link de domínio próprio falha Chamada UIApplication.open no próprio domínio iOS 9+ UIApplication.shared.open chamado no próprio host Evitar abrir a si mesmo; usar roteador interno
Esquema Customizado falha Protocolo não-HTTP não reconhecido pelo WebKit iOS 9+ Esquema não delegado ao UIApplication Interceptar no delegate, validar, abrir via UIApplication
Iframe Hijacking Subframe disparando esquema externo iOS 9+ sourceFrame.isMainFrame não verificado Proteger com if !sourceFrame.isMainFrame e suprimir esquemas não-web
Problema de transição de UI Transições executadas fora da main thread iOS 9+ Falta de despacho para a main-actor Garantir execução na main-actor para roteador e transições

Perguntas Frequentes (FAQ)

Como desenvolvedores podem interceptar Universal Links em um WKWebView?
Desenvolvedores implementam `WKNavigationDelegate` e inspecionam URLs recebidas dentro de `decidePolicyForNavigationAction`. Se a URL corresponder a um destino próprio do app, o delegate cancela a navegação na web view com `.cancel` e passa os parâmetros validados diretamente para o roteador interno do aplicativo.
Posso usar UIApplication.shared.open para abrir meu próprio app a partir de um WKWebView?
A documentação da Apple especifica que chamar `UIApplication.shared.open()` em um Universal Link que aponta para o domínio associado do próprio aplicativo não abrirá o link no app como um Universal Link. Para links próprios, o app deve cancelar a navegação da web view e invocar seu roteador interno diretamente.
Como prevenir que iframes embutidos em um WKWebView disparem lançamentos de apps externos?
Para prevenir o iframe hijacking, verifique `navigationAction.sourceFrame.isMainFrame` dentro de `decidePolicyForNavigationAction`. Se `isMainFrame` for `false`, permita a navegação HTTP(S) padrão com `.allow`, mas cancele esquemas customizados não-web com `.cancel` para evitar que iframes de terceiros executem lançamentos externos não solicitados.

Resumo e framework de decisão

Lidar com Universal Links e esquemas personalizados dentro de um WKWebView no iOS exige preencher a lacuna entre o container de renderização do WebKit e os ciclos de vida de navegação nativos do UIKit. Depender de políticas padrão de navegação pode impedir transições suaves quando uma lógica de roteamento própria é necessária.

Ao implementar um WKNavigationDelegate robusto que verifica limites de frames, reforça políticas conservadoras de ativação de links em handoffs externos, analisa domínios associados internos via validadores rigorosos e delega alvos externos com segurança para UIApplication.shared.open, equipes de engenharia mantêm uma navegação controlada enquanto se protegem contra o iframe protocol hijacking.

Para explorar arquiteturas de deep linking e roteamento de parâmetros nativos no iOS, consulte a documentação de integração do SDK.

Materiais relacionados

Share this article