Tüm Yazılar
KategoriiOS
Okuma Süresi
9 dk
Yayın Tarihi
2026-10-15
Kelime Sayısı
1.425kelime

Kahveni hazırla - bu içerikli bir makale!

AppClipsStudio 2.0: App Clip invocation'ını doğrulamak, yönlendirmek ve boyutunu kontrol etmek

Özet

AppClipsStudio 2.0, App Clip'i açan URL'yi güvenilmeyen girdi sayar, App Store Connect gibi yönlendirir; boyut ve AASA'yı CI'da kontrol eder.

  • Sistem bu URL'yi NSUserActivityTypeBrowsingWeb türünde bir NSUserActivity olarak verir; URL'nin kendisi ise bir App Clip Code'dan, NFC etiketinden, QR koddan, Mesajlar'dan ya da bir web sitesinden gelir.
  • Tek library target, her iş için bir klasör.
  • Examples/CoffeeShop, ortak kaynaklardan derlenen bir uygulama ve App Clip'i; CI bunu master'a yapılan her push'ta ve her pull request'te derliyor.
  • Kaynak kod: [github.com/muhittincamdali/AppClipsStudio](https://github.com/muhittincamdali/AppClipsStudio)
AppClipsStudio 2.0: App Clip invocation'ını doğrulamak, yönlendirmek ve boyutunu kontrol etmek

İçindekiler


Problem

Bir App Clip bir URL ile başlar. Sistem bu URL'yi NSUserActivityTypeBrowsingWeb türünde bir NSUserActivity olarak verir; URL'nin kendisi ise bir App Clip Code'dan, NFC etiketinden, QR koddan, Mesajlar'dan ya da bir web sitesinden gelir. Yani herkesin yazabileceği bir girdidir. Bu URL'nin etrafında sessizce bozulan üç şey var:

  • Yönlendirme. App Store Connect, URL'leri App Clip experience'larına prefix'e göre eşler; App Clip ve tam uygulama da kayıtlı bir prefix altındaki her URL'yi aynı şekilde ele almalı.
  • Boyut. Her variant'ın açılmış (uncompressed) boyutu için sert bir limit var: iOS 15'e kadar 10 MB, iOS 16'da 15 MB, iOS 17 ve sonrasında ise invocation türüne ve deployment target'a göre 100 MB ya da 15 MB. Bunu App Store Connect, yüklemeden sonra kontrol ediyor.
  • Website association. apple-app-site-association dosyasındaki ya da appclips: entitlement girdisindeki bir hata, App Clip'in web sitesinden açılmaması demek. Bunun bir derleme hatası yok.

AppClipsStudio bu üç nokta için yazılmış bir Swift paketi; buna ek olarak konum teyidini, tam uygulama için App Store overlay'ini ve verinin tam uygulamaya aktarılmasını da kapsıyor. appclipstudio adlı komut satırı aracı, App Store Connect'in ve sistemin kontrol edeceklerini yüklemeden önce kontrol ediyor. Paket iOS 16 ve macOS 13'ü destekliyor ve bağımlılığı yok.

2.0.0 bu kodun ilk sürümü. Repo Ağustos 2025'te açıldı ve Şubat 2026'da 1.0.0 etiketlendi. 2.0.0 changelog'unun ifadesiyle 1.0.0, kaynakları placeholder olan modüler bir framework anlatıyordu (AI routing, analytics, güvenlik, networking, storage, UI modülleri). Haziran 2026'da bu modüller birkaç stub'a indirildi; bunların arasında her ödemeyi onaylayan bir Apple Pay sarmalayıcısı ve rastgele boyut basan bir komut satırı aracı vardı. 15 Ekim 2026'da PR #7 bunların hepsini Apple'ın App Clip dokümantasyonu üzerine kurulan kodla değiştirdi ve v2.0.0 merge commit'ine etiketlendi. 1.0.0 API'sinden hiçbir şey kalmadı; sürümün major olmasının nedeni bu.

Tasarım kararları ve alternatifler

Tek library target, her iş için bir klasör. Sources/AppClipsStudio yaklaşık 2.000 satır ve Invocation, Routing, Size, AppClipCode, Association, Handoff, Location ve Promotion klasörlerine bölünmüş. App Clip target'ı tek bir product ekliyor. Her iş için ayrı product, uygulamanın daha az kod bağlamasını sağlayabilirdi; ama bu boyutta çoğunlukla gereksiz ek yük getirirdi.

URL, uygulama kodu görmeden doğrulanıyor. Invocation(url:limits:) sabit bir sırayla, ilk başarısız kontrolü fırlatıyor: uzunluk (varsayılan 2.048 byte), URLComponents ile ayrıştırma, https scheme'i, host varlığı, . ya da .. içermeyen en fazla 32 path bileşeni ve en fazla 64 query item. Limitler InvocationLimits içinde. Decode edilmiş host, path ve query'de kontrol ya da format karakteri olamaz, çünkü bunlar log'a ya da ekrana ulaşabilir. URLComponents'a güvenmek ise aşağıda anlatılan hatanın ta kendisi.

Typed throws. Hatalar throws(InvocationError), throws(ThinningReportError) gibi tanımlanıyor; böylece çağıran her durumu switch ile ele alabiliyor: bildirimden ya da App Switcher'dan gelen açılış için .missingURL, beklenmedik bir host'tan gelen URL için .hostNotAllowed. Bedeli güncel bir toolchain: Swift tools 6.2 ve Xcode 26 ya da üstü. Düz throws eski derleyicilerle de çalışırdı ama her çağıranı cast yapmaya zorlardı.

Navigasyon yapmayan, değer döndüren bir router. InvocationRouter<Destination> uygulamanın kendi ekran tipi üzerinde generic ve yalnızca "bu URL hangi hedefe gidiyor?" sorusunu cevaplıyor. Route'lar, App Store Connect'in prefix eşleştirmesini taklit ederek özgüllük sırasıyla tutuluyor: önce daha çok bileşenli olan, sonra daha çok sabit bileşenli olan, sonra tam eşleşen pattern /* ile bitenden önce, sonra host'lu pattern. Eşitlikte eklenme sırası korunuyor. Handler bir eşleşmeyi nil döndürerek reddedebiliyor (örneğin :id sayı değilse); router o zaman bir sonraki route'u, en sonda da fallback'i deniyor. Hata fırlatmak da "bu benim işim değil" demenin bir yolu olurdu, ama o zaman her çağıranın kendi yeniden deneme döngüsünü yazması gerekirdi. Sonuç düz bir değer olduğundan yönlendirme == ile test ediliyor.

Boyut klasörden değil, Xcode'un yazdığı rapordan. Xcode'un App Thinning Size Report.txt dosyası her variant'ın açılmış boyutunu listeliyor; Apple'ın limitlediği de bu. ThinningReport raporu parse ediyor, SizeBudget ise her variant için limiti iOS sürümüne, deployment target'a ve InvocationSupport değerine (digitalOnly, physical ya da demoLink) göre seçiyor. Megabayt, rapordaki gibi ondalık sayılıyor. BundleSizeScanner derlenmiş bir .app klasörünü de toplayabiliyor, ama dokümantasyonu bu sonuca tahmin diyor.

Kütüphane olarak yazılmış bir komut satırı aracı. Komutlar AppClipsStudioTool target'ında, Tool.run(_:) -> ToolResult olarak duruyor; bu fonksiyon yazdırıp çıkmak yerine çıktıyı ve exit code'u döndürüyor, böylece testler onu süreç başlatmadan çağırıyor. Exit code'lar CI için bir sözleşme: 0 geçti, 1 bir kontrol başarısız, 2 kullanım ya da girdi hatası. Beş komut var (size, check-url, check-domain, aasa validate, aasa generate); aasa generate dışındakiler --json alıyor. Argümanlar, paketi bağımlılıksız tutmak için Swift Argument Parser yerine elle yazılmış küçük bir parser ile ayrıştırılıyor; bedeli daha sade bir --help.

Ham UserDefaults yerine bir handoff zarfı. HandoffStore<Value>, Codable bir değeri App Group'un defaults suite'inde bir şema sürümü, isteğe bağlı bir son kullanma tarihi ve bir boyut limitiyle (varsayılan 64 KB) birlikte saklıyor. Okurken hiçbir şeyi decode etmeden önce boyutu, sonra başlığı, en son değeri kontrol ediyor; take() değeri tam uygulamaya bir kez veriyor. Düz UserDefaults'un, App Clip'in tam uygulamanın okuduğundan daha yeni bir şema yazdığı duruma bir cevabı yok. Dokümantasyon bu yolla sır saklanmamasını söylüyor.

Gerçek bir hata: yalnızca path kontrol ediliyordu

Yeniden yazımdaki ilk Invocation, kontrol ve format karakterlerini yalnızca decode edilmiş path bileşenlerinde arıyordu. Oysa URLComponents host'u ve query'yi de percent-decode ediyor. 583949e commit'i neyin geçtiğini anlatıyor: https://ex%0Aample.com/a, host'u "ex\nample.com" olarak kabul ediliyordu; çift yönlü bir override (%E2%80%AE) host'ta ya da bir query değerinde geçebiliyordu; varsayılan App Clip linkindeki p item'ı da içeriği ne olursa olsun bundle identifier olarak aktarılıyordu.

Düzeltme, doğrulama sınırını URL'nin tamamına yayıyor ve üç yeni hata durumu ekliyor. InvocationError.invalidHost host'u, invalidQueryItem ise item'ın adını yazıldığı haliyle, yani hâlâ percent-encoded olarak taşıyor; böylece hatanın kendisi gizli karakteri bir log'a taşımıyor. invalidBundleIdentifier, AppIdentifier'ın bundle identifier kontrolünü yeniden kullanıyor. InvocationTests artık https://ex%0Aample.com/a → .invalidHost("ex%0Aample.com") gibi girdileri sabitliyor.

Aynı örüntü komut satırı aracında da çıktı. Metin çıktısı gizli karakterleri zaten değiştiriyordu; ama JSONEncoder yalnızca C0 kontrol karakterlerini escape ediyor. Bir terminalin kaçış dizisinin başlangıcı olarak okuduğu U+009B gibi C1 karakterleri, çift yönlü override'lar ve U+2028/U+2029 ayırıcıları --json kullanıldığında terminale olduğu gibi gidiyordu. 2e2f3b3 commit'i bunları \u kaçış dizisine çeviriyor. Bu düzeltme de eksikti: sürümden sonra master'a giren 0727dbc DEL karakterini (U+007F) de escape ediyor; ilk düzeltme yalnızca ASCII dışı scalar'lara baktığı için onu atlamıştı.

Aynı pull request'teki üçüncü düzeltme maliyetle ilgiliydi. 9bd6374 commit'i ThinningReport'u satır satır okuyacak şekilde değiştirdi. Commit mesajı şu ölçümü kaydediyor: 8 MB'lık limitteki, yalnızca satır sonlarından oluşan bir rapor, release derlemesinde değişiklikten önce tepe noktada 696 MB bellek kullanıyordu; sonra 25 MB.

Ders: bir doğrulama sınırı, girdinin decode edildiği ya da dışarı yazıldığı yerlerin listesi kadar geniştir; bir girdi limiti de izin verilen en büyük girdinin maliyeti ölçülmeden yetmez.

Kullanım

README'deki quick start, App Clip'in ContentView.swift dosyasının yerine geçiyor; aynı kod Tests/AppClipsStudioTests/ReadmeQuickStartTests.swift içinde derlenip test ediliyor:

swift
1import AppClipsStudio
2import SwiftUI
3 
4enum Screen: Equatable {
5 case welcome
6 case table(Int)
7}
8 
9struct ContentView: View {
10 @State private var screen = Screen.welcome
11 
12 var body: some View {
13 Group {
14 switch screen {
15 case .welcome: Text("Scan a table tag")
16 case .table(let number): Text("Table \(number)")
17 }
18 }
19 .font(.largeTitle)
20 .onInvocation { invocation in
21 screen = (try? Self.router.route(invocation)) ?? .welcome
22 }
23 }
24 
25 static let router: InvocationRouter<Screen> = {
26 var router = InvocationRouter<Screen>()
27 try? router.add("/table/:number") { match in
28 match.integer("number").map(Screen.table)
29 }
30 router.fallback { _ in .welcome }
31 return router
32 }()
33}

Scheme'deki _XCAppClipURL değeri https://appclip.example.com/table/12 olduğunda iPhone simülatöründe "Table 12" görünüyor; /table/twelve ise "Scan a table tag" gösteriyor, çünkü integer(_:) nil dönüyor ve fallback devreye giriyor.

CI'da boyut kontrolü job'ı exit status ile düşürüyor:

text
1$ appclipstudio size "App Thinning Size Report.txt" --deployment-target 16.0
2App Clip size check (iOS 16+, physical and digital invocations)
3 ok ExampleClip-a.ipa 14.60 MB of 15.00 MB
4 OVER ExampleClip-b.ipa 15.20 MB of 15.00 MB
51 of 2 variants are over the limit.

Examples/CoffeeShop, ortak kaynaklardan derlenen bir uygulama ve App Clip'i; CI bunu master'a yapılan her push'ta ve her pull request'te derliyor.

Sınırlar

  • Gerçek cihaz ve gerçek domain ile kontrol yok. Örnek, simülatörde example.com host'larını kullanıyor; orada activation payload olmadığı için konum teyidi "kontrol edilmedi" gösteriyor. Testler sistem sonuçlarının LocationConfirmation'a nasıl eşlendiğini kapsıyor, sistem çağrısını değil.
  • Apple'ın sayıları koda yazılı. Boyut limitleri ve App Clip Code kuralları Apple'ın dokümantasyonundan alındı; Apple bunları değiştirirse paketin elle güncellenmesi gerekiyor.
  • Klasör taraması bir tahmin. Kesin boyut yalnızca Xcode'un boyut raporundan geliyor; dışa aktarılmış bir .ipa'yı doğrudan okumak yol haritasında.
  • Tam uygulamanın kimliği tahmin ediliyor. AASA doğrulayıcısı, Xcode'un adlandırmasını izleyerek App Clip kimliğinin son bileşenini atıyor; farklı adlandırılmış bir projede gereksiz bir uyarı çıkabilir.
  • Yeni kod. 2.0.0, 15 Ekim 2026'da yayımlandı. Etiketteki test süitinde 119 Swift Testing testi var (macOS'ta swift test ile ölçüldü), ama repo dışında kullanıldığına dair henüz bir kayıt yok.

Kaynak kod: github.com/muhittincamdali/AppClipsStudio

Etiketler

#swift#app-clips#ios#swiftui#universal-links#swift-package
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

12+ yıllık deneyime sahip Senior Mobile Engineer. iOS, Flutter ve React Native ile mobil ürünler geliştiriyorum; full-stack ve AI-native çalışıyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Onay e-postasındaki bağlantıyı açıp “Aboneliğimi onayla” düğmesine bastığında aboneliğin başlar. Bültende açılma/tıklama istatistikleri tutulur; dilediğin an tek tıkla ayrılabilirsin. Gizlilik

Paylaş

İlgili İçerik