No post anterior, configuramos o Android para capturar deep links usando Kotlin. Agora é a vez do iOS — e você vai ver como a implementação em Swift pode ficar bastante limpa.
Dando continuidade à série sobre Deep Links no Flutter, agora vamos implementar o fluxo nativo no iOS. Se você ainda não viu os posts anteriores, recomendo começar por aqui: Post 1 — Guia para Iniciantes | Post 2 — Android com Kotlin.
Neste artigo, você vai aprender:
- Como configurar Info.plist e Associated Domains para deep links.
- A diferença entre custom schemes e Universal Links no iOS.
- Como implementar AppDelegate.swift com FlutterMethodChannel e FlutterEventChannel.
Configurando o Info.plist
No iOS, o registro de um custom scheme é feito no Info.plist, por meio da chave CFBundleURLTypes. É aqui que o sistema operacional aprende que links com o scheme fitconnect:// devem abrir este app.
<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.fitconnect.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>fitconnect</string>
</array>
</dict>
</array>
CFBundleURLSchemes é o que de fato registra o scheme. O CFBundleURLName é um identificador interno — por convenção, usa-se o bundle ID reverso. O CFBundleTypeRole como Editor indica que o app não apenas lê o tipo de URL, mas também pode criar links com esse scheme.
Configurando Universal Links
Universal Links exigem um passo a mais: o app precisa declarar quais domínios ele está autorizado a tratar. Essa declaração vai no arquivo Runner.entitlements, sob a chave com.apple.developer.associated-domains.
<!-- ios/Runner/Runner.entitlements -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:fitconnect.app</string>
</array>
O prefixo applinks: é obrigatório — é ele que instrui o iOS a tratar o domínio como Universal Link. Sem ele, o sistema ignora a entrada. A verificação bidirecional (o arquivo apple-app-site-association no servidor) será montada no Post 5.
Implementando o AppDelegate.swift
Com a configuração pronta, o iOS entrega os links ao AppDelegate. Diferente do Android, onde a entrada costuma ser centralizada em torno do Intent, no iOS o link pode chegar por caminhos diferentes: no cold start via launchOptions, por custom schemes via open url e por Universal Links via continue userActivity.
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
private let CHANNEL = "com.fitconnect.app/deeplink"
private let EVENT_CHANNEL = "com.fitconnect.app/deeplink_stream"
private var methodChannel: FlutterMethodChannel?
private var eventChannel: FlutterEventChannel?
private var eventSink: FlutterEventSink?
private var initialLink: String?
private var lastProcessedLink: String?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
setupChannels(controller: controller)
// Captura deep link inicial (cold start)
if let url = launchOptions?[.url] as? URL {
handleDeepLink(url: url, isInitial: true)
} else if
let userActivityDictionary = launchOptions?[.userActivityDictionary] as? [AnyHashable: Any] {
for value in userActivityDictionary.values {
if let userActivity = value as? NSUserActivity,
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
handleDeepLink(url: url, isInitial: true)
break
}
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func setupChannels(controller: FlutterViewController) {
// MethodChannel: Flutter pede o deep link inicial
methodChannel = FlutterMethodChannel(
name: CHANNEL,
binaryMessenger: controller.binaryMessenger
)
methodChannel?.setMethodCallHandler { [weak self] call, result in
if call.method == "getInitialLink" {
result(self?.initialLink)
} else {
result(FlutterMethodNotImplemented)
}
}
// EventChannel: stream de deep links em tempo real enquanto o app está em execução
eventChannel = FlutterEventChannel(
name: EVENT_CHANNEL,
binaryMessenger: controller.binaryMessenger
)
eventChannel?.setStreamHandler(self)
}
// Custom URL Scheme
override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
handleDeepLink(url: url, isInitial: false)
return super.application(app, open: url, options: options)
}
// Universal Links
override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
handleDeepLink(url: url, isInitial: false)
return true
}
private func handleDeepLink(url: URL, isInitial: Bool) {
let urlString = url.absoluteString
// Evita processar o mesmo link duas vezes
if urlString == lastProcessedLink { return }
lastProcessedLink = urlString
NSLog("Deep link: \(urlString)")
if isInitial {
initialLink = urlString
} else {
eventSink?(urlString)
}
}
}
extension AppDelegate: FlutterStreamHandler {
func onListen(
withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink
) -> FlutterError? {
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}
Por que dois métodos de entrada?
-
open url: chamado quando o link usa um custom scheme (fitconnect://). O iOS já sabe que é para abrir o app porque o scheme foi registrado noInfo.plist. -
continue userActivity: chamado para Universal Links (HTTPS). O iOS só chega aqui após confirmar que o domínio está noRunner.entitlementse que o servidor possui o arquivoapple-app-site-association. Se qualquer verificação falhar, o link abre no Safari como fallback — sem erros, sem seletor.
Por que o AppDelegate adota FlutterStreamHandler via extension?
Em vez de criar uma classe separada para o stream handler, o próprio AppDelegate implementa o protocolo FlutterStreamHandler via extension. Isso mantém o código coeso: tudo relacionado à entrega de links fica no mesmo lugar. O onListen guarda a referência para o eventSink, e o onCancel a limpa quando o Flutter para de ouvir.
Testando no iOS
Para testar sem precisar de um device físico, o simulador do Xcode expõe o comando xcrun simctl openurl.
# Custom scheme — não requer verificação de domínio
xcrun simctl openurl booted \
"fitconnect://fitconnect.app/signup?referralCode=TRAINER1234567890123"
# Universal Link (HTTPS)
xcrun simctl openurl booted \
"https://fitconnect.app/signup?referralCode=TRAINER1234567890123"
Atenção: testar Universal Links no iOS pode ser enganoso. Em muitos casos, abrir a URL diretamente no Safari não reproduz o comportamento real de associação com o app. Para validar corretamente, prefira tocar no link a partir de apps como Mail, Notas ou iMessage.
Android vs iOS: comparação rápida
A lógica é a mesma nas duas plataformas — o que muda são as APIs e os arquivos de configuração. O handleDeepLink no Swift é o equivalente direto do handleIntent no Kotlin.
O que construímos até aqui
Ao final desta etapa, você já tem:
- O
Info.plistconfigurado para custom schemes. - O
Runner.entitlementscom o domínio para Universal Links. - O
AppDelegate.swiftcomFlutterMethodChannel(link inicial) eFlutterEventChannel(stream em tempo real). - Clareza sobre a diferença entre
open urlecontinue userActivitye quando cada um é chamado. - Comandos
xcrun simctlprontos para testar no simulador.
Este é o terceiro de 9 posts da série. Com Android e iOS prontos, temos o código nativo completo das duas plataformas. Se você já passou por algum problema com Universal Links — domínio não verificado, link abrindo no Safari em vez do app — conta nos comentários, esse tipo de detalhe vai direto para os próximos artigos.
No próximo post, conectamos tudo no Flutter: o DeepLinkService em Dart que fala com o código nativo via MethodChannel e EventChannel.
Código completo disponível no repositório: FitConnect no GitHub
Se esse conteúdo te ajudou, deixa um clap 👏 e salva o post — isso me ajuda a continuar a série.
Já configurou Universal Links antes? Conta nos comentários o maior obstáculo — vou usar isso nos próximos artigos.
Tags: Flutter, iOS, Swift, Universal Links, Deep Links


Top comments (0)