# Deep Linking ve Universal Links: Kapsamlı Rehber 🔗
Merhaba! Bugün iOS uygulamalarının en kritik konularından biri olan deep linking'i A'dan Z'ye inceleyeceğiz. Bu rehber sonunda kullanıcılarını doğrudan uygulamanın istediğin yerine yönlendirebileceksin.
İçindekiler
- Deep Linking Nedir?
- Custom URL Schemes (Legacy)
- Universal Links (Recommended)
- Universal Link Handler
- SwiftUI Entegrasyonu
- Deferred Deep Linking
- Debug ve Troubleshooting
- Sürpriz Hediye: Universal Links Checklist
- Pro Tips ve Best Practices
- Easter Egg: Secret Debug Mode
- Sonuç
🎯 Bu Yazıda Öğreneceklerin
- Custom URL Schemes vs Universal Links farkları
- Apple App Site Association (AASA) dosyası kurulumu
- SwiftUI ve UIKit entegrasyonu
- Deferred deep linking stratejileri
- Branch.io, Firebase Dynamic Links alternatifleri
- Debug ve troubleshooting teknikleri
📚 Deep Linking Nedir?
Deep linking, kullanıcıları bir web URL'i veya custom scheme aracılığıyla uygulamanın belirli bir ekranına yönlendirme tekniğidir.
Deep Link Türleri
Tür | Örnek | Avantaj | Dezavantaj |
|---|---|---|---|
Custom URL Scheme | `myapp://product/123` | Kolay kurulum | Güvenlik riski |
Universal Links | `https://myapp.com/product/123` | Güvenli, fallback | Sunucu gerekli |
App Links (Android) | `https://myapp.com/...` | Cross-platform | Android-only |
💡 Altın İpucu: 2024'te Universal Links kullan. Custom URL scheme'ler artık güvenlik açığı kabul ediliyor!
Dış Kaynaklar:
- Apple: Supporting Universal Links
- WWDC22: What's new in App Clips
- Apple: Allowing Apps to Link to Your Content
🔧 Custom URL Schemes (Legacy)
1. Info.plist Konfigürasyonu
xml
12CFBundleURLTypes 3 4 5 CFBundleURLSchemes 6 7 myapp 8 myapp-staging 9 10 CFBundleURLName 11 com.mycompany.myapp 12 CFBundleTypeRole 13 Editor 14 1516 1718LSApplicationQueriesSchemes 19 20 twitter 21 instagram 22 fb 232. URL Handling (UIKit)
swift
1// AppDelegate.swift2import UIKit3 4class AppDelegate: UIResponder, UIApplicationDelegate {5 6 // iOS 13 öncesi - deprecated ama hala çalışır7 func application(8 _ app: UIApplication,9 open url: URL,10 options: [UIApplication.OpenURLOptionsKey: Any] = [:]11 ) -> Bool {12 13 // Kaynak app bilgisi14 let sourceApp = options[.sourceApplication] as? String15 print("📲 URL opened from: \(sourceApp ?? "unknown")")16 17 // Deep link handler'a gönder18 return DeepLinkHandler.shared.handle(url: url, source: .customScheme)19 }20}21 22// SceneDelegate.swift (iOS 13+)23class SceneDelegate: UIResponder, UIWindowSceneDelegate {24 25 func scene(26 _ scene: UIScene,27 openURLContexts URLContexts: Set<UIOpenURLContext>28 ) {29 guard let context = URLContexts.first else { return }30 31 let url = context.url32 let options = context.options33 34 print("📲 Scene opened URL: \(url)")35 print(" Source: \(options.sourceApplication ?? "unknown")")36 37 DeepLinkHandler.shared.handle(url: url, source: .customScheme)38 }39}🌐 Universal Links (Recommended)
3. Associated Domains Entitlement
swift
1// Xcode > Signing & Capabilities > + Capability > Associated Domains2// Format: applinks:yourdomain.com3 4/*5Associated Domains:6- applinks:myapp.com7- applinks:www.myapp.com8- applinks:staging.myapp.com9- webcredentials:myapp.com (AutoFill için)10*/4. Apple App Site Association (AASA) Dosyası
json
1// https://myapp.com/.well-known/apple-app-site-association2// VEYA https://myapp.com/apple-app-site-association3// NOT: .json uzantısı KULLANMA, Content-Type: application/json olmalı4 5{6 "applinks": {7 "apps": [],8 "details": [9 {10 "appIDs": [11 "TEAMID.com.mycompany.myapp",12 "TEAMID.com.mycompany.myapp.staging"13 ],14 "components": [15 {16 "/": "/product/*",17 "comment": "Tüm ürün sayfaları"18 },19 {20 "/": "/user/*",21 "comment": "Kullanıcı profilleri"22 },23 {24 "/": "/article/*",25 "?": { "ref": "?*" },26 "comment": "Makaleler (query params dahil)"27 },28 {29 "/": "/promo/*",30 "comment": "Promosyon linkleri"31 },32 {33 "/": "/share/*",34 "comment": "Paylaşım linkleri"35 },36 {37 "/": "/",38 "exclude": true,39 "comment": "Ana sayfa hariç"40 },41 {42 "/": "/blog/*",43 "exclude": true,44 "comment": "Blog web'de kalsın"45 }46 ]47 }48 ]49 },50 "webcredentials": {51 "apps": ["TEAMID.com.mycompany.myapp"]52 },53 "appclips": {54 "apps": ["TEAMID.com.mycompany.myapp.Clip"]55 }56}5. AASA Doğrulama Checklist
bash
1# AASA dosyasını kontrol et2curl -I https://myapp.com/.well-known/apple-app-site-association3 4# Beklenen response:5# HTTP/2 2006# Content-Type: application/json7# (Redirect olmamalı!)8 9# Apple'ın CDN'inden kontrol et10curl "https://app-site-association.cdn-apple.com/a/v1/myapp.com"11 12# 🐣 Easter Egg: Apple'ın debug tool'u13# https://search.developer.apple.com/appsearch-validation-tool/14# (Apple Developer hesabı gerekli)📱 Universal Link Handler
6. Deep Link Router
swift
1import Foundation2 3// MARK: - Deep Link Types4enum DeepLink: Equatable {5 case product(id: String)6 case user(username: String)7 case article(slug: String, referrer: String?)8 case promo(code: String)9 case share(type: ShareType, id: String)10 case settings(section: SettingsSection?)11 case search(query: String?)12 case unknown(url: URL)13 14 enum ShareType: String {15 case product, article, playlist16 }17 18 enum SettingsSection: String {19 case account, notifications, privacy, appearance20 }21}22 23// MARK: - Deep Link Source24enum DeepLinkSource {25 case customScheme26 case universalLink27 case pushNotification28 case spotlight29 case siri30 case widget31 case appClip32}33 34// MARK: - Deep Link Handler35final class DeepLinkHandler {36 static let shared = DeepLinkHandler()37 38 // Pending deep link (uygulama tam açılmadan önce)39 private(set) var pendingDeepLink: DeepLink?40 41 // Deep link callback42 var onDeepLinkReceived: ((DeepLink, DeepLinkSource) -> Void)?43 44 private init() {}45 46 // MARK: - Handle URL47 @discardableResult48 func handle(url: URL, source: DeepLinkSource) -> Bool {49 let deepLink = parse(url: url)50 51 // Unknown ise false dön52 if case .unknown = deepLink {53 print("⚠️ Unknown deep link: \(url)")54 return false55 }56 57 // Analytics event58 trackDeepLink(deepLink, source: source)59 60 // Callback veya pending61 if let callback = onDeepLinkReceived {62 callback(deepLink, source)63 } else {64 pendingDeepLink = deepLink65 }66 67 return true68 }69 70 // MARK: - Parse URL71 func parse(url: URL) -> DeepLink {72 // Custom scheme: myapp://product/12373 // Universal link: https://myapp.com/product/12374 75 let pathComponents = url.pathComponents.filter { $0 != "/" }76 let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems77 78 guard let firstComponent = pathComponents.first else {79 return .unknown(url: url)80 }81 82 switch firstComponent {83 case "product", "p":84 if let id = pathComponents.dropFirst().first {85 return .product(id: id)86 }87 88 case "user", "u", "profile":89 if let username = pathComponents.dropFirst().first {90 return .user(username: username)91 }92 93 case "article", "a", "post":94 if let slug = pathComponents.dropFirst().first {95 let referrer = queryItems?.first { $0.name == "ref" }?.value96 return .article(slug: slug, referrer: referrer)97 }98 99 case "promo", "redeem":100 if let code = pathComponents.dropFirst().first {101 return .promo(code: code)102 }103 // Query param olarak da gelebilir104 if let code = queryItems?.first(where: { $0.name == "code" })?.value {105 return .promo(code: code)106 }107 108 case "share":109 if let typeStr = pathComponents.dropFirst().first,110 let type = DeepLink.ShareType(rawValue: typeStr),111 let id = pathComponents.dropFirst(2).first {112 return .share(type: type, id: id)113 }114 115 case "settings":116 let sectionStr = pathComponents.dropFirst().first117 let section = sectionStr.flatMap { DeepLink.SettingsSection(rawValue: $0) }118 return .settings(section: section)119 120 case "search":121 let query = queryItems?.first { $0.name == "q" }?.value122 return .search(query: query)123 124 default:125 break126 }127 128 return .unknown(url: url)129 }130 131 // MARK: - Consume Pending132 func consumePendingDeepLink() -> DeepLink? {133 defer { pendingDeepLink = nil }134 return pendingDeepLink135 }136 137 // MARK: - Analytics138 private func trackDeepLink(_ deepLink: DeepLink, source: DeepLinkSource) {139 let eventParams: [String: Any] = [140 "deep_link_type": String(describing: deepLink),141 "source": String(describing: source)142 ]143 // Analytics.logEvent("deep_link_opened", parameters: eventParams)144 print("📊 Deep link tracked: \(eventParams)")145 }146}🎨 SwiftUI Entegrasyonu
7. Modern SwiftUI Deep Link Handler
swift
1import SwiftUI2 3// MARK: - App Entry Point4@main5struct MyApp: App {6 @StateObject private var router = DeepLinkRouter()7 @StateObject private var appState = AppState()8 9 var body: some Scene {10 WindowGroup {11 ContentView()12 .environmentObject(router)13 .environmentObject(appState)14 // Universal Links15 .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in16 guard let url = activity.webpageURL else { return }17 router.handle(url: url, source: .universalLink)18 }19 // Custom URL Schemes20 .onOpenURL { url in21 router.handle(url: url, source: .customScheme)22 }23 // Pending deep link (cold start)24 .task {25 await router.processPendingDeepLink()26 }27 }28 }29}30 31// MARK: - Deep Link Router (SwiftUI)32@MainActor33final class DeepLinkRouter: ObservableObject {34 // Navigation state35 @Published var selectedTab: AppTab = .home36 @Published var navigationPath = NavigationPath()37 38 // Sheets39 @Published var presentedSheet: AppSheet?40 41 // Specific destinations42 @Published var productToShow: String?43 @Published var userToShow: String?44 @Published var articleToShow: String?45 @Published var promoToApply: String?46 47 // Alert for promo codes48 @Published var promoAlert: PromoAlert?49 50 func handle(url: URL, source: DeepLinkSource) {51 let deepLink = DeepLinkHandler.shared.parse(url: url)52 navigate(to: deepLink, source: source)53 }54 55 func navigate(to deepLink: DeepLink, source: DeepLinkSource) {56 // Reset navigation state57 navigationPath = NavigationPath()58 59 switch deepLink {60 case .product(let id):61 selectedTab = .shop62 // Delayed navigation for tab switch animation63 Task {64 try? await Task.sleep(for: .milliseconds(100))65 productToShow = id66 }67 68 case .user(let username):69 selectedTab = .profile70 Task {71 try? await Task.sleep(for: .milliseconds(100))72 userToShow = username73 }74 75 case .article(let slug, let referrer):76 selectedTab = .home77 Task {78 try? await Task.sleep(for: .milliseconds(100))79 articleToShow = slug80 if let referrer {81 // Track referrer for attribution82 print("📊 Referrer: \(referrer)")83 }84 }85 86 case .promo(let code):87 // Show promo code alert88 promoToApply = code89 presentedSheet = .promo(code: code)90 91 case .share(let type, let id):92 switch type {93 case .product:94 productToShow = id95 case .article:96 articleToShow = id97 case .playlist:98 // Handle playlist99 break100 }101 102 case .settings(let section):103 selectedTab = .settings104 if let section {105 Task {106 try? await Task.sleep(for: .milliseconds(100))107 navigationPath.append(section)108 }109 }110 111 case .search(let query):112 selectedTab = .search113 if let query {114 // Pre-fill search query115 NotificationCenter.default.post(116 name: .searchQueryReceived,117 object: nil,118 userInfo: ["query": query]119 )120 }121 122 case .unknown:123 // Fallback to home124 selectedTab = .home125 }126 }127 128 func processPendingDeepLink() async {129 // Cold start'ta bekleyen deep link var mı?130 if let pending = DeepLinkHandler.shared.consumePendingDeepLink() {131 // UI hazır olana kadar bekle132 try? await Task.sleep(for: .milliseconds(500))133 navigate(to: pending, source: .customScheme)134 }135 }136}137 138// MARK: - Supporting Types139enum AppTab: String, CaseIterable {140 case home, shop, search, profile, settings141}142 143enum AppSheet: Identifiable {144 case promo(code: String)145 case share(item: Any)146 147 var id: String {148 switch self {149 case .promo(let code): return "promo-\(code)"150 case .share: return "share"151 }152 }153}154 155struct PromoAlert: Identifiable {156 let id = UUID()157 let code: String158 let message: String159}160 161extension Notification.Name {162 static let searchQueryReceived = Notification.Name("searchQueryReceived")163}8. Navigation Destination Handler
swift
1// MARK: - Content View with Deep Link Navigation2struct ContentView: View {3 @EnvironmentObject var router: DeepLinkRouter4 5 var body: some View {6 TabView(selection: $router.selectedTab) {7 HomeTab()8 .tabItem { Label("Ana Sayfa", systemImage: "house") }9 .tag(AppTab.home)10 11 ShopTab()12 .tabItem { Label("Mağaza", systemImage: "bag") }13 .tag(AppTab.shop)14 15 SearchTab()16 .tabItem { Label("Ara", systemImage: "magnifyingglass") }17 .tag(AppTab.search)18 19 ProfileTab()20 .tabItem { Label("Profil", systemImage: "person") }21 .tag(AppTab.profile)22 23 SettingsTab()24 .tabItem { Label("Ayarlar", systemImage: "gear") }25 .tag(AppTab.settings)26 }27 .sheet(item: $router.presentedSheet) { sheet in28 switch sheet {29 case .promo(let code):30 PromoRedeemView(code: code)31 case .share(let item):32 ShareSheet(items: [item])33 }34 }35 }36}37 38// MARK: - Shop Tab with Deep Link39struct ShopTab: View {40 @EnvironmentObject var router: DeepLinkRouter41 @State private var products: [Product] = []42 43 var body: some View {44 NavigationStack(path: $router.navigationPath) {45 ProductListView(products: products)46 .navigationTitle("Mağaza")47 .navigationDestination(for: String.self) { productId in48 ProductDetailView(productId: productId)49 }50 // Deep link'ten gelen product51 .onChange(of: router.productToShow) { _, productId in52 if let productId {53 router.navigationPath.append(productId)54 router.productToShow = nil55 }56 }57 }58 }59}📦 Deferred Deep Linking
9. First Install Attribution
swift
1// MARK: - Deferred Deep Link Manager2final class DeferredDeepLinkManager {3 static let shared = DeferredDeepLinkManager()4 5 private let defaults = UserDefaults.standard6 private let pendingLinkKey = "pendingDeepLink"7 private let installDateKey = "appInstallDate"8 private let attributionWindowSeconds: TimeInterval = 7 * 24 * 60 * 60 // 7 gün9 10 private init() {11 recordInstallDateIfNeeded()12 }13 14 // MARK: - Install Date15 private func recordInstallDateIfNeeded() {16 if defaults.object(forKey: installDateKey) == nil {17 defaults.set(Date(), forKey: installDateKey)18 }19 }20 21 var isFirstLaunch: Bool {22 // İlk launch kontrolü için daha güvenilir yöntem23 let hasLaunchedBefore = defaults.bool(forKey: "hasLaunchedBefore")24 if !hasLaunchedBefore {25 defaults.set(true, forKey: "hasLaunchedBefore")26 return true27 }28 return false29 }30 31 // MARK: - Save Pending Deep Link32 /// Uygulama yüklenmeden önce yakalanan deep link'i sakla33 func savePendingDeepLink(_ url: URL) {34 let data: [String: Any] = [35 "url": url.absoluteString,36 "timestamp": Date().timeIntervalSince197037 ]38 defaults.set(data, forKey: pendingLinkKey)39 print("💾 Pending deep link saved: \(url)")40 }41 42 // MARK: - Handle Pending Deep Link43 /// Uygulama açıldığında bekleyen deep link'i işle44 @MainActor45 func handlePendingDeepLink() async -> DeepLink? {46 guard let data = defaults.dictionary(forKey: pendingLinkKey),47 let urlString = data["url"] as? String,48 let timestamp = data["timestamp"] as? TimeInterval,49 let url = URL(string: urlString) else {50 return nil51 }52 53 // Attribution window kontrolü54 let savedDate = Date(timeIntervalSince1970: timestamp)55 guard Date().timeIntervalSince(savedDate) < attributionWindowSeconds else {56 // Çok eski, temizle57 clearPendingDeepLink()58 return nil59 }60 61 // İşle ve temizle62 clearPendingDeepLink()63 64 return DeepLinkHandler.shared.parse(url: url)65 }66 67 // MARK: - Clear68 func clearPendingDeepLink() {69 defaults.removeObject(forKey: pendingLinkKey)70 }71 72 // MARK: - Clipboard Check (iOS 14+)73 /// Clipboard'da deep link var mı kontrol et74 @MainActor75 func checkClipboardForDeepLink() async -> DeepLink? {76 // iOS 14+ clipboard access uyarısı gösterir77 // Kullanıcıdan izin gerekebilir78 79 guard UIPasteboard.general.hasURLs,80 let url = UIPasteboard.general.url,81 url.host == "myapp.com" else {82 return nil83 }84 85 return DeepLinkHandler.shared.parse(url: url)86 }87}88 89// 💡 Pro Tip: Branch.io veya AppsFlyer ile deferred deep linking90// çok daha güvenilir çalışır. Fingerprinting kullanırlar.🧪 Debug ve Troubleshooting
10. Universal Links Debug Tool
swift
1#if DEBUG2// MARK: - Debug Tools3struct UniversalLinksDebugView: View {4 @State private var testURL = "https://myapp.com/product/123"5 @State private var parseResult = ""6 @State private var aasaStatus = ""7 8 var body: some View {9 Form {10 Section("Test URL") {11 TextField("URL", text: $testURL)12 .textInputAutocapitalization(.never)13 .autocorrectionDisabled()14 15 Button("Parse") {16 testParse()17 }18 19 if !parseResult.isEmpty {20 Text(parseResult)21 .font(.caption)22 .foregroundStyle(.secondary)23 }24 }25 26 Section("AASA Status") {27 Button("Check AASA") {28 Task { await checkAASA() }29 }30 31 if !aasaStatus.isEmpty {32 Text(aasaStatus)33 .font(.caption)34 }35 }36 37 Section("Quick Tests") {38 Button("Open Product Deep Link") {39 openURL("myapp://product/test123")40 }41 Button("Open Universal Link") {42 openURL("https://myapp.com/product/test123")43 }44 }45 46 Section("Info") {47 LabeledContent("Bundle ID", value: Bundle.main.bundleIdentifier ?? "-")48 LabeledContent("Team ID", value: "YOUR_TEAM_ID")49 }50 }51 .navigationTitle("Deep Link Debug")52 }53 54 private func testParse() {55 guard let url = URL(string: testURL) else {56 parseResult = "❌ Invalid URL"57 return58 }59 60 let deepLink = DeepLinkHandler.shared.parse(url: url)61 parseResult = "✅ Parsed: \(deepLink)"62 }63 64 private func checkAASA() async {65 guard let url = URL(string: "https://myapp.com/.well-known/apple-app-site-association") else {66 return67 }68 69 do {70 let (data, response) = try await URLSession.shared.data(from: url)71 72 if let httpResponse = response as? HTTPURLResponse {73 if httpResponse.statusCode == 200 {74 let json = try JSONSerialization.jsonObject(with: data)75 aasaStatus = "✅ Valid AASA\n\(json)"76 } else {77 aasaStatus = "❌ HTTP \(httpResponse.statusCode)"78 }79 }80 } catch {81 aasaStatus = "❌ Error: \(error.localizedDescription)"82 }83 }84 85 private func openURL(_ string: String) {86 guard let url = URL(string: string) else { return }87 UIApplication.shared.open(url)88 }89}90#endifmarkdown
1# 🎁 UNIVERSAL LINKS PRODUCTION CHECKLIST2 3## Domain Setup4- [ ] HTTPS aktif (zorunlu)5- [ ] SSL sertifikası valid6- [ ] AASA dosyası doğru path'te7- [ ] Content-Type: application/json8- [ ] Redirect yok (200 OK)9- [ ] CDN cache kontrol edildi10 11## AASA File12- [ ] JSON syntax valid13- [ ] appIDs doğru format (TEAMID.bundleid)14- [ ] paths doğru tanımlanmış15- [ ] exclude paths kontrol edildi16- [ ] webcredentials eklendi (opsiyonel)17- [ ] appclips eklendi (opsiyonel)18 19## Xcode Setup20- [ ] Associated Domains capability eklendi21- [ ] applinks:domain.com doğru format22- [ ] Staging/production ayrı domain'ler23- [ ] Entitlements dosyası commit edildi24 25## Code Implementation26- [ ] onOpenURL handler (SwiftUI)27- [ ] application(_:continue:) (UIKit)28- [ ] scene(_:openURLContexts:) (SceneDelegate)29- [ ] Deep link parser30- [ ] Navigation router31- [ ] Deferred deep link support32- [ ] Error handling33 34## Testing35- [ ] Simulator'da Notes app ile test36- [ ] Gerçek cihazda Safari ile test37- [ ] iMessage ile test38- [ ] Cold start test39- [ ] Warm start test40- [ ] Associated Domains refresh (Developer Mode)41 42## Analytics43- [ ] Deep link tracking44- [ ] Source attribution45- [ ] Conversion tracking46- [ ] Error logging47 48## Troubleshooting KomutlarıOkuyucu Ödülü
# AASA kontrol
curl -I https://domain.com/.well-known/apple-app-site-association
# Apple CDN kontrol
curl https://app-site-association.cdn-apple.com/a/v1/domain.com
# Cihazda log
# Console.app > Action > Include Info/Debug Messages
# Filtrelele: "swcd" veya "associated"
swift
1 💡 Pro Tips ve Best Practices
- Universal Links > Custom URL Schemes - Güvenlik ve UX için her zaman Universal Links tercih et.
- AASA CDN Cache - Apple AASA'yı cache'ler. Güncelleme yapınca 24-48 saat bekle.
- Fallback URL - App yüklü değilse web sitene yönlendir.
- Deep Link Analytics - Hangi deep link'lerin en çok kullanıldığını takip et.
- A/B Test - Farklı landing page'ler için farklı deep link path'leri kullan.
- QR Code - Marketing materyallerinde Universal Link QR kodları kullan.
- Smart App Banner - Web sitene
<meta name="apple-itunes-app">ekle.
- Branch.io/Firebase - Deferred deep linking için 3rd party SDK'lar daha güvenilir.
swift
1// 5 kez ayarlar ikonuna tıklayınca debug menüsü açılır2struct SettingsTab: View {3 @State private var tapCount = 04 @State private var showDebug = false5 6 var body: some View {7 NavigationStack {8 // ... settings content ...9 }10 .onTapGesture(count: 5) {11 showDebug = true12 }13 .sheet(isPresented: $showDebug) {14 UniversalLinksDebugView()15 }16 }17}📖 Sonuç
Deep linking, modern mobil uygulamalar için vazgeçilmez bir özellik. Universal Links ile güvenli, kullanıcı dostu deneyimler sunabilir ve marketing kampanyalarının etkinliğini ölçebilirsin.
Bir sonraki yazıda görüşmek üzere! 🔗
*Sorularını Twitter'da sorabilirsin!*
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.

