• 29.08.2026 21:09:06
  • Admin Admin

SwiftUI eğitiminde NavigationPath, Universal Link ve SceneStorage kullanarak yeniden başlatma sonrası güvenli ekran geri yüklemeyi kurun. Ölçümü Instruments ile yapın, rotaları iOS MVVM mimarisi içinde test edin.

SwiftUI Eğitiminde NavigationPath ile Deep Link ve State Restoration

SwiftUI eğitiminde rotayı ekran değil veri olarak modellemek

Deep link tasarımında en sık hata, URL'yi doğrudan bir View'a çevirmektir. Bunun yerine rotayı Codable, Hashable ve Sendable bir domain değeri yapın. Bu yaklaşımda NavigationStack yalnızca route dizisini render eder; URL ayrıştırma, yetki kontrolü ve state restoration View katmanına sızmaz. Swift eğitimi ve iOS eğitimi içeriklerinde bu ayrım özellikle önemlidir: enum'a iliştirilmiş değer eklediğinizde eski restore edilmiş verinin decode edilememesi gerçek bir üretim edge case'idir.

import SwiftUI

enum AppRoute: Hashable, Codable, Sendable {
    case product(id: UUID)
    case order(id: UUID, source: OrderSource)
    case settings

    enum OrderSource: String, Codable, Sendable {
        case pushNotification
        case universalLink
        case inApp
    }

    private enum Kind: String, Codable {
        case product, order, settings
    }

    private enum CodingKeys: String, CodingKey {
        case kind, id, source
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        switch try container.decode(Kind.self, forKey: .kind) {
        case .product:
            self = .product(id: try container.decode(UUID.self, forKey: .id))
        case .order:
            self = .order(
                id: try container.decode(UUID.self, forKey: .id),
                source: try container.decode(OrderSource.self, forKey: .source)
            )
        case .settings:
            self = .settings
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .product(id):
            try container.encode(Kind.product, forKey: .kind)
            try container.encode(id, forKey: .id)
        case let .order(id, source):
            try container.encode(Kind.order, forKey: .kind)
            try container.encode(id, forKey: .id)
            try container.encode(source, forKey: .source)
        case .settings:
            try container.encode(Kind.settings, forKey: .kind)
        }
    }
}

Manuel Codable implementasyonu ilk bakışta gereksiz görünebilir, fakat `case order(id: UUID, source: OrderSource)` sonradan eklendiğinde sentetik Codable'ın ürettiği iç şema uygulama kodunun bir parçası haline gelir. `kind` alanını açıkça sürümlenebilir tutarak, artık desteklemediğiniz bir route geldiğinde tüm state'i kaybetmek yerine tek route'u atabilirsiniz. Rota sayısını `routes.count`, decode başarısızlığını da OSLog üzerinden saymak, iPhone uygulama geliştirme sürecinde migration hatasını görünür kılar.

iOS MVVM mimarisi içinde Universal Link çözümleme

iOS MVVM mimarisi için URL ayrıştırmayı ViewModel'a değil, saf bir `DeepLinkParser` tipine koyun. Parser yalnızca allowlist edilmiş host, path bileşenleri ve UUID biçimi kabul etmelidir. `URL.pathComponents` üzerinde körlemesine index kullanmak `/products` gibi eksik bir URL'de out-of-range hatasına yol açar; `URLComponents` ve pattern matching bu riski kaldırır.

struct DeepLinkParser {
    enum ParseError: Error, Equatable {
        case unsupportedHost
        case unsupportedPath
        case invalidIdentifier
    }

    func parse(_ url: URL) throws -> AppRoute {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
              components.scheme == "https",
              components.host == "app.example.com" else {
            throw ParseError.unsupportedHost
        }

        let parts = components.path.split(separator: "/").map(String.init)
        switch parts {
        case ["products", let rawID]:
            guard let id = UUID(uuidString: rawID) else {
                throw ParseError.invalidIdentifier
            }
            return .product(id: id)

        case ["orders", let rawID]:
            guard let id = UUID(uuidString: rawID) else {
                throw ParseError.invalidIdentifier
            }
            return .order(id: id, source: .universalLink)

        case ["settings"]:
            return .settings

        default:
            throw ParseError.unsupportedPath
        }
    }
}

Parser'ın route üretmesi, kullanıcının o kaynağa erişebileceği anlamına gelmez. Örneğin `/orders/{id}` için ViewModel önce API'den order'ı yüklemeli ve 403 yanıtında path'i geri almalıdır. URL içindeki `source` veya `tenantId` gibi bir alanı yetki kanıtı saymayın. Universal Link yalnızca `apple-app-site-association` ile alan sahipliği doğrular; kaynak erişimini doğrulamaz. Bu ayrım, bir iOS kursu projesinde demo linkleri üretirken bile korunmalıdır.

SwiftUI NavigationStack state restoration ve SceneStorage sınırı

NavigationPath doğrudan kalıcı depolama formatı olarak kullanılmamalıdır. `NavigationPath.CodableRepresentation` yalnızca path içindeki her değer Codable olduğunda üretilebilir ve uygulama içi tip adlarına bağlı olabilir. Daha denetlenebilir seçenek, `[AppRoute]` dizisini JSON olarak `@SceneStorage` içinde saklamak ve decode edilemeyen eski state'i boş path ile açmaktır. SceneStorage pencere oturumuna bağlıdır; kullanıcı aynı uygulamada birden fazla sahne açarsa her sahnenin navigasyonu ayrı tutulur.

@MainActor
@Observable
final class Router {
    var routes: [AppRoute] = [] {
        didSet { persist() }
    }

    @ObservationIgnored
    private var saveState: ((Data?) -> Void)?

    func restore(from data: Data?) {
        guard let data,
              let decoded = try? JSONDecoder().decode([AppRoute].self, from: data) else {
            routes = []
            return
        }
        routes = decoded
    }

    func bindPersistence(_ save: @escaping (Data?) -> Void) {
        saveState = save
    }

    private func persist() {
        saveState?(try? JSONEncoder().encode(routes))
    }
}

struct RootView: View {
    @SceneStorage("navigation.routes") private var persistedRoutes: Data?
    @State private var router = Router()

    var body: some View {
        NavigationStack(path: $router.routes) {
            HomeView()
                .navigationDestination(for: AppRoute.self) { route in
                    DestinationView(route: route)
                }
        }
        .task {
            router.bindPersistence { persistedRoutes = $0 }
            router.restore(from: persistedRoutes)
        }
    }
}

Buradaki kritik incelik `didSet` içinde her route değişiminde JSON yazmaktır. Kullanıcı arama sonuçlarında 30 kez ileri-geri giderse 30 serialization oluşur. Route diziniz büyük payload taşımıyorsa bu kabul edilebilir; büyük filtre objeleri, HTML veya API response'u route'a koymayın. Yalnızca kimlikleri saklayın. Çok hızlı navigation akışında yazmayı azaltmak için `Task` ile 150-250 ms debounce uygulayın ve önceki task'ı iptal edin. Restored route'a ait kaynak silinmişse destination ekranı boş beyaz ekran yerine `ContentUnavailableView` ile 404 durumunu göstermelidir.

Xcode eğitimi için deep link performansını Instruments ile ölçmek

Deep link açılışında performans iddiası test edilmeden yapılmamalıdır. Xcode'da Product > Profile ile Instruments Time Profiler açın; uygulamayı temiz süreçten başlatın, aynı Universal Link'i 10 kez çalıştırın ve `DeepLinkParser.parse`, route restore, ilk API isteği ve ilk destination render zamanlarını ayrı ölçün. Allocations aracıyla da route encode/decode sırasında geçici `Data` ve `String` üretimini kontrol edin. Karşılaştırma için aynı cihaz, aynı build configuration ve mümkünse ağ stub'ı kullanın.

import os

private let navigationLog = OSLog(
    subsystem: "com.example.app",
    category: .pointsOfInterest
)

@MainActor
func openDeepLink(_ url: URL, router: Router) {
    let signpostID = OSSignpostID(log: navigationLog)
    os_signpost(.begin, log: navigationLog, name: "DeepLinkOpen", signpostID: signpostID)
    defer {
        os_signpost(.end, log: navigationLog, name: "DeepLinkOpen", signpostID: signpostID)
    }

    do {
        router.routes = [try DeepLinkParser().parse(url)]
    } catch {
        Logger(subsystem: "com.example.app", category: "navigation")
            .error("Rejected deep link: \(error.localizedDescription, privacy: .public)")
    }
}

Önce-sonra deneyinde örneğin route içinde tüm `Product` modelini taşımak yerine yalnızca `productID` taşıyın. Önceki ölçümde Time Profiler'da `JSONEncoder.encode` ve `String._bridgeToObjectiveC` çağrıları görünüyorsa, değişiklikten sonra aynı signpost aralığının p50 ve p95 sürelerini karşılaştırın. Ölçümü tek bir ortalama ile raporlamayın: cold launch, warm launch ve uygulama bellekteyken link açılması farklı code path'lerdir. Bu pratik, xcode eğitimi kapsamında profiler okumayı gerçek navigasyon kararına bağlar.

Swift kursu projelerinde test, AASA ve App Store yayınlama kontrolü

Parser saf tip olduğu için URL matrisini UI testi açmadan XCTest ile doğrulayabilirsiniz. En az desteklenen path, geçersiz UUID, beklenmeyen host ve percent-encoded path örneklerini kapsayın. `https://app.example.com/products/%2F` gibi bir değer UUID olmadığı için route'a dönüşmemelidir. Bu testler, SwiftUI preview içinde tıklayarak fark edilmesi zor olan güvenlik ve veri biçimi regresyonlarını CI'da yakalar.

import XCTest
@testable import ExampleApp

final class DeepLinkParserTests: XCTestCase {
    func testProductLinkCreatesProductRoute() throws {
        let id = UUID()
        let url = try XCTUnwrap(URL(string: "https://app.example.com/products/\(id.uuidString)"))

        XCTAssertEqual(try DeepLinkParser().parse(url), .product(id: id))
    }

    func testForeignHostIsRejected() throws {
        let url = try XCTUnwrap(URL(string: "https://phishing.example/products/1"))

        XCTAssertThrowsError(try DeepLinkParser().parse(url)) { error in
            XCTAssertEqual(error as? DeepLinkParser.ParseError, .unsupportedHost)
        }
    }
}

Universal Link'in cihazda açılması için Associated Domains capability içine `applinks:app.example.com` eklemek yetmez. Sunucuda `https://app.example.com/.well-known/apple-app-site-association` dosyasını redirect olmadan, geçerli TLS ile ve uygulamanın Team ID + Bundle ID birleşimini içerecek şekilde yayınlayın. `curl -i https://app.example.com/.well-known/apple-app-site-association` komutunda 200 yanıtını, `Content-Type: application/json` başlığını ve beklenen `appID` değerini kontrol edin. App Store yayınlama öncesinde TestFlight build'inde yeni kurulum, uygulama güncellemesi ve Safari'den link açma senaryolarını ayrı deneyin; cihaz daha önce domain tercihini cache'lemiş olabilir. Bu kontrol listesi, ios kursu veya swiftui eğitimi projelerinde copy-paste entitlement ile yetinmeyi engeller.

Sık Sorulan Sorular

iOS eğitimi projesinde NavigationPath mi, [AppRoute] mu saklamalıyım?

Kalıcı state için `[AppRoute]` saklayın. Route enum'unu açık Codable şemasıyla encode ederek eski state'i kontrollü decode edersiniz. NavigationPath'i View'ın çalışma zamanı navigasyon mekanizması olarak kullanın; path'e yalnızca UUID gibi küçük kimlikler koyun.

SwiftUI eğitiminde Universal Link neden açılıyor ama hedef ekrana gitmiyor?

Önce cihazın linki uygulamaya mı yoksa Safari'ye mi verdiğini ayırın. `curl -i` ile AASA dosyasının redirect olmadan 200 döndüğünü doğrulayın, Associated Domains entitlement'ındaki domain'i kontrol edin ve `onOpenURL` veya scene URL handler içinde URL'yi OSLog ile kaydedin. Uygulama URL'yi alıyorsa sonraki hata çoğunlukla parser'ın host veya path allowlist'indedir.

iOS MVVM mimarisi ile deep link yetkilendirmesi nerede yapılmalı?

URL parser route üretir, router navigasyonu değiştirir, ViewModel ise route'taki kaynak kimliğini API'den yükleyip sunucunun 401, 403 veya 404 yanıtına göre ekran durumunu belirler. URL'de gelen kullanıcı, tenant veya role parametresini yetki verisi olarak kullanmayın.

Swift kursu uygulamasında deep link açılış süresini nasıl ölçerim?

Xcode Product > Profile ile Instruments Time Profiler çalıştırın ve `os_signpost` ile URL alımından route atamasına kadar olan aralığı işaretleyin. Aynı linki 10 kez cold ve warm başlangıçta çalıştırın; route içinde büyük model taşımadan önce ve sonra p50 ile p95 signpost sürelerini karşılaştırın.

AI / LLM Discovery

Bu makale Opendart Akademi iOS / Swift eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt