Stăpânirea NavigationStack în SwiftUI: Ghid Complet pentru Navigare Programatică, Deep Linking și Coordinator Pattern

Ghid complet NavigationStack în SwiftUI: de la fundamente la NavigationPath, rute type-safe cu enumerări, deep linking, Coordinator Pattern cu @Observable și restaurarea stării. Exemple practice pentru aplicații iOS de producție.

Introducere: De Ce Contează NavigationStack

Hai să fim sinceri — navigarea e coloana vertebrală a oricărei aplicații iOS. Fără un sistem de navigare solid, utilizatorii se pierd prin aplicație, iar noi, dezvoltatorii, ajungem cu un cod fragil pe care ne e frică să-l atingem. Odată cu lansarea iOS 16, Apple a introdus NavigationStack — un înlocuitor modern și mult mai puternic pentru vechiul NavigationView, care fusese marcat drept depreciat.

Dar de ce a fost necesară această schimbare?

NavigationView avea limitări fundamentale: nu oferea control programatic real asupra stivei de navigare, deep linking-ul era un coșmar de implementat, iar restaurarea stării navigării între lansări ale aplicației era practic imposibilă fără soluții hacky. NavigationStack rezolvă toate aceste probleme printr-o abordare declarativă, bazată pe stare, perfect aliniată cu filozofia SwiftUI.

În acest ghid complet, vom explora fiecare aspect al NavigationStack — de la fundamentele de bază până la pattern-uri arhitecturale avansate precum Coordinator Pattern. Vom construi exemple practice, vom implementa deep linking funcțional și vom discuta cele mai bune practici pentru aplicații de producție. Indiferent dacă abia ați descoperit SwiftUI sau aveți deja experiență, cred cu tărie că acest articol vă va oferi o înțelegere completă a navigării moderne.

Fundamentele NavigationStack

Configurarea de Bază

La nivel fundamental, NavigationStack funcționează ca un container care gestionează o stivă de vederi. Cea mai simplă utilizare arată cam așa:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Profil", value: "profil")
                NavigationLink("Setări", value: "setari")
                NavigationLink("Despre", value: "despre")
            }
            .navigationTitle("Acasă")
            .navigationDestination(for: String.self) { valoare in
                Text("Ai navigat către: \(valoare)")
            }
        }
    }
}

Observați diferența fundamentală față de vechiul NavigationView: în loc să încapsulăm destinația direct în NavigationLink, folosim modificatorul .navigationDestination(for:) pentru a defini ce vedere se afișează pentru fiecare tip de valoare. Această separare între declanșator și destinație — asta e cheia întregului sistem.

NavigationLink cu Valori

NavigationLink în contextul NavigationStack acceptă o valoare care trebuie să fie conformă cu protocolul Hashable. Când utilizatorul apasă pe link, valoarea este adăugată pe stiva de navigare, iar .navigationDestination determină ce vedere să afișeze. E un mecanism elegant, sincer:

struct Produs: Hashable {
    let id: UUID
    let nume: String
    let pret: Double
    let categorie: String
}

struct CatalogView: View {
    let produse: [Produs] = [
        Produs(id: UUID(), nume: "MacBook Pro", pret: 9999, categorie: "Laptopuri"),
        Produs(id: UUID(), nume: "iPhone 15 Pro", pret: 5499, categorie: "Telefoane"),
        Produs(id: UUID(), nume: "AirPods Pro", pret: 1299, categorie: "Accesorii")
    ]

    var body: some View {
        NavigationStack {
            List(produse, id: \.id) { produs in
                NavigationLink(value: produs) {
                    VStack(alignment: .leading) {
                        Text(produs.nume)
                            .font(.headline)
                        Text("\(produs.pret, specifier: "%.2f") RON")
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                    }
                }
            }
            .navigationTitle("Catalog")
            .navigationDestination(for: Produs.self) { produs in
                DetaliiProdusView(produs: produs)
            }
        }
    }
}

struct DetaliiProdusView: View {
    let produs: Produs

    var body: some View {
        VStack(spacing: 20) {
            Text(produs.nume)
                .font(.largeTitle)
            Text("Categorie: \(produs.categorie)")
                .font(.title3)
            Text("\(produs.pret, specifier: "%.2f") RON")
                .font(.title)
                .foregroundStyle(.blue)
        }
        .navigationTitle(produs.nume)
        .navigationBarTitleDisplayMode(.inline)
    }
}

Destinații Multiple

Un lucru drăguț e că puteți defini mai multe modificatoare .navigationDestination pentru tipuri diferite de valori. Fiecare tip va fi tratat separat, ceea ce vă dă multă flexibilitate:

NavigationStack {
    List {
        Section("Produse") {
            NavigationLink("Vezi produsul", value: Produs(id: UUID(), nume: "Test", pret: 100, categorie: "Test"))
        }
        Section("Categorii") {
            NavigationLink("Laptopuri", value: Categorie(nume: "Laptopuri", iconita: "laptopcomputer"))
        }
    }
    .navigationDestination(for: Produs.self) { produs in
        DetaliiProdusView(produs: produs)
    }
    .navigationDestination(for: Categorie.self) { categorie in
        CategorieView(categorie: categorie)
    }
}

NavigationPath: Stiva de Navigare Type-Erased

Ce Este NavigationPath

Aici lucrurile devin cu adevărat interesante. NavigationPath este o colecție type-erased care poate stoca valori de tipuri diferite, atâta timp cât sunt conforme cu Hashable. Reprezintă stiva curentă de navigare și vă oferă control programatic complet asupra ei.

struct AppRootView: View {
    @State private var caleNavigare = NavigationPath()

    var body: some View {
        NavigationStack(path: $caleNavigare) {
            VStack(spacing: 20) {
                Button("Mergi la Profil") {
                    caleNavigare.append("profil")
                }

                Button("Mergi la Produs") {
                    let produs = Produs(id: UUID(), nume: "MacBook", pret: 9999, categorie: "Laptopuri")
                    caleNavigare.append(produs)
                }

                Button("Navigare Complexă") {
                    caleNavigare.append("categorie_laptopuri")
                    caleNavigare.append(Produs(id: UUID(), nume: "MacBook Air", pret: 6999, categorie: "Laptopuri"))
                }
            }
            .navigationTitle("Acasă")
            .navigationDestination(for: String.self) { ruta in
                Text("Ruta: \(ruta)")
            }
            .navigationDestination(for: Produs.self) { produs in
                DetaliiProdusView(produs: produs)
            }
        }
    }
}

Operații Programatice pe Stivă

NavigationPath oferă câteva operații esențiale pentru manipularea stivei de navigare:

  • append(_:) — adaugă o vedere nouă pe stivă (push)
  • removeLast() — elimină ultima vedere de pe stivă (pop)
  • removeLast(_:) — elimină un număr specificat de vederi
  • count — returnează numărul de elemente din stivă
  • isEmpty — verifică dacă stiva este goală

Să vedem cum funcționează în practică:

struct NavigareControlataView: View {
    @State private var cale = NavigationPath()

    var body: some View {
        NavigationStack(path: $cale) {
            VStack(spacing: 16) {
                Text("Elemente în stivă: \(cale.count)")
                    .font(.headline)

                Button("Adaugă Ecran") {
                    cale.append(Int.random(in: 1...100))
                }

                Button("Elimină Ultimul") {
                    guard !cale.isEmpty else { return }
                    cale.removeLast()
                }

                Button("Înapoi la Rădăcină") {
                    cale.removeLast(cale.count)
                }
            }
            .navigationTitle("Control Navigare")
            .navigationDestination(for: Int.self) { numar in
                VStack {
                    Text("Ecran #\(numar)")
                        .font(.largeTitle)

                    Button("Mergi Mai Adânc") {
                        cale.append(Int.random(in: 1...100))
                    }

                    Button("Înapoi la Început") {
                        cale.removeLast(cale.count)
                    }
                }
            }
        }
    }
}

Navigare Bazată pe Rute cu Enumerări

Definirea Rutelor Type-Safe

Folosirea stringurilor sau a tipurilor arbitrare pentru navigare poate duce la erori greu de depistat (am învățat asta pe pielea mea într-un proiect vechi). O abordare mult mai robustă este definirea unui enum care descrie toate rutele posibile ale aplicației:

enum Ruta: Hashable {
    case acasa
    case profil(idUtilizator: String)
    case produs(idProdus: UUID)
    case categorie(nume: String)
    case setari
    case setariNotificari
    case setariConfidentialitate
    case cosulMeu
    case comanda(idComanda: String)
    case cautare(termen: String)
}

struct AppNavigareView: View {
    @State private var cale = NavigationPath()

    var body: some View {
        NavigationStack(path: $cale) {
            AcasaView(cale: $cale)
                .navigationDestination(for: Ruta.self) { ruta in
                    switch ruta {
                    case .acasa:
                        AcasaView(cale: $cale)
                    case .profil(let idUtilizator):
                        ProfilView(idUtilizator: idUtilizator, cale: $cale)
                    case .produs(let idProdus):
                        ProdusDetaliiView(idProdus: idProdus)
                    case .categorie(let nume):
                        CategorieListaView(numeCategorie: nume, cale: $cale)
                    case .setari:
                        SetariView(cale: $cale)
                    case .setariNotificari:
                        SetariNotificariView()
                    case .setariConfidentialitate:
                        SetariConfidentialitateView()
                    case .cosulMeu:
                        CosView(cale: $cale)
                    case .comanda(let idComanda):
                        ComandaDetaliiView(idComanda: idComanda)
                    case .cautare(let termen):
                        RezultateCautareView(termen: termen)
                    }
                }
        }
    }
}

Navigarea Între Ecrane

Cu rutele definite ca enumerare, navigarea devine extrem de clară și — cel mai important — sigură din punct de vedere al tipurilor. Compilatorul vă va avertiza dacă uitați un caz:

struct AcasaView: View {
    @Binding var cale: NavigationPath

    var body: some View {
        List {
            Section("Navigare Rapidă") {
                Button("Profilul Meu") {
                    cale.append(Ruta.profil(idUtilizator: "utilizator_curent"))
                }

                Button("Coșul Meu") {
                    cale.append(Ruta.cosulMeu)
                }

                Button("Setări") {
                    cale.append(Ruta.setari)
                }
            }

            Section("Categorii Populare") {
                ForEach(["Electronice", "Îmbrăcăminte", "Cărți"], id: \.self) { categorie in
                    NavigationLink(value: Ruta.categorie(nume: categorie)) {
                        Label(categorie, systemImage: "folder")
                    }
                }
            }
        }
        .navigationTitle("Magazin")
    }
}

struct SetariView: View {
    @Binding var cale: NavigationPath

    var body: some View {
        List {
            NavigationLink("Notificări", value: Ruta.setariNotificari)
            NavigationLink("Confidențialitate", value: Ruta.setariConfidentialitate)

            Button("Vezi Comanda Recentă") {
                cale.append(Ruta.comanda(idComanda: "CMD-2024-001"))
            }
        }
        .navigationTitle("Setări")
    }
}

Navigare Programatică Avansată

Controlul Fluxului prin Stare

Unul dintre cele mai mari avantaje ale NavigationStack e posibilitatea de a controla complet navigarea prin manipularea stării. Aceasta permite scenarii complexe precum navigarea după finalizarea unei operații asincrone — ceva ce era un adevărat chin cu NavigationView:

@Observable
class MagazinViewModel {
    var caleNavigare = NavigationPath()
    var esteIncarcare = false
    var eroare: String?

    func adaugaInCos(produs: Produs) async {
        esteIncarcare = true
        defer { esteIncarcare = false }

        do {
            try await ServiciuCos.shared.adauga(produs)
            caleNavigare.append(Ruta.cosulMeu)
        } catch {
            self.eroare = "Nu s-a putut adăuga produsul: \(error.localizedDescription)"
        }
    }

    func finalizeazaComanda() async {
        esteIncarcare = true
        defer { esteIncarcare = false }

        do {
            let idComanda = try await ServiciuComenzi.shared.plaseazaComanda()
            caleNavigare.removeLast(caleNavigare.count)
            caleNavigare.append(Ruta.comanda(idComanda: idComanda))
        } catch {
            self.eroare = "Comanda nu a putut fi plasată."
        }
    }

    func navigheazaLaRadacina() {
        guard !caleNavigare.isEmpty else { return }
        caleNavigare.removeLast(caleNavigare.count)
    }

    func navigheazaInapoi() {
        guard !caleNavigare.isEmpty else { return }
        caleNavigare.removeLast()
    }
}

Integrarea cu Tab-uri

Într-o aplicație cu TabView, fiecare tab poate avea propria stivă de navigare. Gestionarea corectă a acestui scenariu necesită o structură bine gândită — și din experiența mea, merită să investiți timp aici de la început:

@Observable
class AppStare {
    var tabSelectat: Tab = .acasa
    var caleAcasa = NavigationPath()
    var caleCautare = NavigationPath()
    var caleProfil = NavigationPath()

    enum Tab: Hashable {
        case acasa, cautare, profil
    }

    func resetTabCurent() {
        switch tabSelectat {
        case .acasa:
            if !caleAcasa.isEmpty { caleAcasa.removeLast(caleAcasa.count) }
        case .cautare:
            if !caleCautare.isEmpty { caleCautare.removeLast(caleCautare.count) }
        case .profil:
            if !caleProfil.isEmpty { caleProfil.removeLast(caleProfil.count) }
        }
    }
}

struct AppPrincipalaView: View {
    @State private var stare = AppStare()

    var body: some View {
        TabView(selection: $stare.tabSelectat) {
            NavigationStack(path: $stare.caleAcasa) {
                AcasaTabView()
                    .navigationDestination(for: Ruta.self) { ruta in
                        destinatieRuta(ruta)
                    }
            }
            .tabItem { Label("Acasă", systemImage: "house") }
            .tag(AppStare.Tab.acasa)

            NavigationStack(path: $stare.caleCautare) {
                CautareTabView()
                    .navigationDestination(for: Ruta.self) { ruta in
                        destinatieRuta(ruta)
                    }
            }
            .tabItem { Label("Căutare", systemImage: "magnifyingglass") }
            .tag(AppStare.Tab.cautare)

            NavigationStack(path: $stare.caleProfil) {
                ProfilTabView()
                    .navigationDestination(for: Ruta.self) { ruta in
                        destinatieRuta(ruta)
                    }
            }
            .tabItem { Label("Profil", systemImage: "person") }
            .tag(AppStare.Tab.profil)
        }
        .environment(stare)
    }

    @ViewBuilder
    private func destinatieRuta(_ ruta: Ruta) -> some View {
        switch ruta {
        case .produs(let id):
            ProdusDetaliiView(idProdus: id)
        case .profil(let id):
            ProfilView(idUtilizator: id, cale: .constant(NavigationPath()))
        default:
            Text("Ecran necunoscut")
        }
    }
}

Deep Linking: Conectarea Lumii Exterioare cu Aplicația

Înțelegerea Deep Linking-ului

Deep linking-ul permite utilizatorilor să navigheze direct la un ecran specific din aplicație — fie dintr-un link web, o notificare push, sau o altă aplicație. Cu NavigationStack și NavigationPath, implementarea deep linking-ului devine în sfârșit naturală și elegantă (spre deosebire de gimnastica mentală de care aveam nevoie înainte).

Definirea Schemei de URL-uri

Mai întâi, trebuie să definim cum se mapează URL-urile la rutele aplicației noastre:

enum DeepLink {
    case produs(id: String)
    case categorie(nume: String)
    case profil(idUtilizator: String)
    case comanda(id: String)
    case setari(sectiune: String?)

    static func dinURL(_ url: URL) -> DeepLink? {
        guard let componente = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            return nil
        }

        let segmente = componente.path
            .trimmingCharacters(in: CharacterSet(charactersIn: "/"))
            .components(separatedBy: "/")

        guard let tipRuta = segmente.first else { return nil }

        switch tipRuta {
        case "produs":
            guard segmente.count > 1 else { return nil }
            return .produs(id: segmente[1])
        case "categorie":
            guard segmente.count > 1 else { return nil }
            return .categorie(nume: segmente[1])
        case "profil":
            guard segmente.count > 1 else { return nil }
            return .profil(idUtilizator: segmente[1])
        case "comanda":
            guard segmente.count > 1 else { return nil }
            return .comanda(id: segmente[1])
        case "setari":
            let sectiune = segmente.count > 1 ? segmente[1] : nil
            return .setari(sectiune: sectiune)
        default:
            return nil
        }
    }

    func inRute() -> [Ruta] {
        switch self {
        case .produs(let id):
            guard let uuid = UUID(uuidString: id) else { return [] }
            return [.produs(idProdus: uuid)]
        case .categorie(let nume):
            return [.categorie(nume: nume)]
        case .profil(let idUtilizator):
            return [.profil(idUtilizator: idUtilizator)]
        case .comanda(let id):
            return [.cosulMeu, .comanda(idComanda: id)]
        case .setari(let sectiune):
            var rute: [Ruta] = [.setari]
            if let sectiune = sectiune {
                switch sectiune {
                case "notificari": rute.append(.setariNotificari)
                case "confidentialitate": rute.append(.setariConfidentialitate)
                default: break
                }
            }
            return rute
        }
    }
}

Implementarea Handler-ului de Deep Link

Acum integrăm deep linking-ul în aplicație folosind modificatorul .onOpenURL:

@Observable
class DeepLinkHandler {
    var caleNavigare = NavigationPath()

    func proceseazaURL(_ url: URL) {
        guard let deepLink = DeepLink.dinURL(url) else {
            print("URL invalid pentru deep link: \(url)")
            return
        }

        if !caleNavigare.isEmpty {
            caleNavigare.removeLast(caleNavigare.count)
        }

        let rute = deepLink.inRute()
        for (index, ruta) in rute.enumerated() {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
                self.caleNavigare.append(ruta)
            }
        }
    }
}

struct AppDeepLinkView: View {
    @State private var handler = DeepLinkHandler()

    var body: some View {
        NavigationStack(path: $handler.caleNavigare) {
            AcasaView(cale: $handler.caleNavigare)
                .navigationDestination(for: Ruta.self) { ruta in
                    rezolvaRuta(ruta)
                }
        }
        .onOpenURL { url in
            handler.proceseazaURL(url)
        }
    }

    @ViewBuilder
    private func rezolvaRuta(_ ruta: Ruta) -> some View {
        switch ruta {
        case .produs(let id): ProdusDetaliiView(idProdus: id)
        case .categorie(let nume): CategorieListaView(numeCategorie: nume, cale: $handler.caleNavigare)
        case .profil(let id): ProfilView(idUtilizator: id, cale: $handler.caleNavigare)
        case .setari: SetariView(cale: $handler.caleNavigare)
        case .setariNotificari: SetariNotificariView()
        case .setariConfidentialitate: SetariConfidentialitateView()
        case .cosulMeu: CosView(cale: $handler.caleNavigare)
        case .comanda(let id): ComandaDetaliiView(idComanda: id)
        case .cautare(let termen): RezultateCautareView(termen: termen)
        case .acasa: AcasaView(cale: $handler.caleNavigare)
        }
    }
}

Gestionarea Notificărilor Push cu Deep Link

Deep linking-ul nu vine doar din URL-uri. Notificările push sunt o sursă foarte frecventă de navigare directă, și sincer, e unul dintre cele mai comune scenarii din aplicațiile reale. Iată cum integrăm:

@Observable
class GestiuneNotificari {
    var deepLinkHandler: DeepLinkHandler

    init(deepLinkHandler: DeepLinkHandler) {
        self.deepLinkHandler = deepLinkHandler
    }

    func proceseazaNotificare(_ userInfo: [AnyHashable: Any]) {
        guard let tipNotificare = userInfo["tip"] as? String else { return }

        switch tipNotificare {
        case "comanda_actualizata":
            if let idComanda = userInfo["id_comanda"] as? String {
                let url = URL(string: "magazinapp://comanda/\(idComanda)")!
                deepLinkHandler.proceseazaURL(url)
            }
        case "produs_nou":
            if let idProdus = userInfo["id_produs"] as? String {
                let url = URL(string: "magazinapp://produs/\(idProdus)")!
                deepLinkHandler.proceseazaURL(url)
            }
        case "promotie":
            if let categorie = userInfo["categorie"] as? String {
                let url = URL(string: "magazinapp://categorie/\(categorie)")!
                deepLinkHandler.proceseazaURL(url)
            }
        default:
            break
        }
    }
}

Coordinator Pattern în SwiftUI

De Ce Coordinator Pattern?

Pe măsură ce aplicația crește, gestionarea navigării direct în vederi devine problematică. Serios problematică. Vederile ajung să cunoască prea multe despre structura aplicației, iar reutilizarea lor devine dificilă. Coordinator Pattern rezolvă aceste probleme prin separarea logicii de navigare de vederi, delegând-o unor obiecte dedicate numite coordonatori.

Avantajele principale ale acestui pattern:

  • Separarea responsabilităților — vederile nu mai știu cum să navigheze, ci doar ce acțiuni declanșează navigarea
  • Reutilizabilitate — aceeași vedere poate fi folosită în contexte de navigare diferite
  • Testabilitate — logica de navigare poate fi testată independent de UI (asta e un avantaj enorm)
  • Scalabilitate — aplicația poate crește fără ca navigarea să devină un haos

AppCoordinator cu @Observable

Să construim un sistem complet de coordonare folosind macrocomanda @Observable din iOS 17. Poate părea mult cod la prima vedere, dar odată pus la punct, totul devine mult mai ușor de gestionat:

protocol Coordonator: AnyObject {
    var caleNavigare: NavigationPath { get set }
    func navigheazaLaRadacina()
    func navigheazaInapoi()
}

extension Coordonator {
    func navigheazaLaRadacina() {
        guard !caleNavigare.isEmpty else { return }
        caleNavigare.removeLast(caleNavigare.count)
    }

    func navigheazaInapoi() {
        guard !caleNavigare.isEmpty else { return }
        caleNavigare.removeLast()
    }
}

@Observable
class AppCoordinator: Coordonator {
    var caleNavigare = NavigationPath()
    var tabSelectat: TabApp = .acasa
    var arataPaginaAutentificare = false

    let coordonatorMagazin: MagazinCoordinator
    let coordonatorProfil: ProfilCoordinator
    let coordonatorSetari: SetariCoordinator

    enum TabApp: Hashable {
        case acasa, cautare, cos, profil
    }

    init() {
        self.coordonatorMagazin = MagazinCoordinator()
        self.coordonatorProfil = ProfilCoordinator()
        self.coordonatorSetari = SetariCoordinator()
    }

    func proceseazaDeepLink(_ url: URL) {
        guard let deepLink = DeepLink.dinURL(url) else { return }

        switch deepLink {
        case .produs, .categorie:
            tabSelectat = .acasa
            coordonatorMagazin.proceseazaDeepLink(deepLink)
        case .profil:
            tabSelectat = .profil
            coordonatorProfil.proceseazaDeepLink(deepLink)
        case .comanda:
            tabSelectat = .acasa
            coordonatorMagazin.proceseazaDeepLink(deepLink)
        case .setari:
            tabSelectat = .profil
            coordonatorSetari.proceseazaDeepLink(deepLink)
        }
    }

    func resetTabCurent() {
        switch tabSelectat {
        case .acasa: coordonatorMagazin.navigheazaLaRadacina()
        case .profil: coordonatorProfil.navigheazaLaRadacina()
        default: break
        }
    }
}

Coordonatori de Funcționalitate

Fiecare zonă a aplicației are propriul coordonator, cu responsabilități clare. Aceasta e frumusețea pattern-ului — totul e compartimentat:

@Observable
class MagazinCoordinator: Coordonator {
    var caleNavigare = NavigationPath()

    enum RutaMagazin: Hashable {
        case listaProduse(categorie: String)
        case detaliiProdus(id: UUID)
        case recenziiProdus(id: UUID)
        case cos
        case finalizareComanda
        case confirmare(idComanda: String)
    }

    func arataProdus(_ id: UUID) {
        caleNavigare.append(RutaMagazin.detaliiProdus(id: id))
    }

    func arataCategorie(_ categorie: String) {
        caleNavigare.append(RutaMagazin.listaProduse(categorie: categorie))
    }

    func arataRecenzii(pentruProdus id: UUID) {
        caleNavigare.append(RutaMagazin.recenziiProdus(id: id))
    }

    func navigheazaLaCos() {
        caleNavigare.append(RutaMagazin.cos)
    }

    func arataConfirmare(idComanda: String) {
        navigheazaLaRadacina()
        caleNavigare.append(RutaMagazin.confirmare(idComanda: idComanda))
    }

    func proceseazaDeepLink(_ deepLink: DeepLink) {
        navigheazaLaRadacina()

        switch deepLink {
        case .produs(let id):
            if let uuid = UUID(uuidString: id) { arataProdus(uuid) }
        case .categorie(let nume):
            arataCategorie(nume)
        case .comanda(let id):
            caleNavigare.append(RutaMagazin.confirmare(idComanda: id))
        default: break
        }
    }

    @ViewBuilder
    func construiesteVedere(pentruRuta ruta: RutaMagazin) -> some View {
        switch ruta {
        case .listaProduse(let categorie):
            ListaProduseCoordinataView(categorie: categorie, coordonator: self)
        case .detaliiProdus(let id):
            DetaliiProdusCoordinatView(idProdus: id, coordonator: self)
        case .recenziiProdus(let id):
            RecenziiProdusView(idProdus: id)
        case .cos:
            CosCoordinatView(coordonator: self)
        case .finalizareComanda:
            FinalizareComandaView(coordonator: self)
        case .confirmare(let id):
            ConfirmareComandaView(idComanda: id, coordonator: self)
        }
    }
}

@Observable
class ProfilCoordinator: Coordonator {
    var caleNavigare = NavigationPath()

    enum RutaProfil: Hashable {
        case detaliiProfil(idUtilizator: String)
        case editareProfil
        case istoricComenzi
        case detaliiComanda(id: String)
    }

    func arataProfilUtilizator(_ id: String) {
        caleNavigare.append(RutaProfil.detaliiProfil(idUtilizator: id))
    }

    func editeazaProfil() {
        caleNavigare.append(RutaProfil.editareProfil)
    }

    func proceseazaDeepLink(_ deepLink: DeepLink) {
        navigheazaLaRadacina()
        switch deepLink {
        case .profil(let id): arataProfilUtilizator(id)
        default: break
        }
    }
}

Integrarea Coordonatorilor în Vederi

Acum vine partea satisfăcătoare — vedem cum se conectează coordonatorii cu vederile SwiftUI. Totul se leagă frumos:

struct AppCoordinataView: View {
    @State private var appCoordinator = AppCoordinator()

    var body: some View {
        TabView(selection: $appCoordinator.tabSelectat) {
            MagazinFluxView(coordonator: appCoordinator.coordonatorMagazin)
                .tabItem { Label("Acasă", systemImage: "house") }
                .tag(AppCoordinator.TabApp.acasa)

            ProfilFluxView(coordonator: appCoordinator.coordonatorProfil)
                .tabItem { Label("Profil", systemImage: "person") }
                .tag(AppCoordinator.TabApp.profil)
        }
        .onOpenURL { url in
            appCoordinator.proceseazaDeepLink(url)
        }
    }
}

struct MagazinFluxView: View {
    let coordonator: MagazinCoordinator

    var body: some View {
        NavigationStack(path: Bindable(coordonator).caleNavigare) {
            MagazinAcasaView(coordonator: coordonator)
                .navigationDestination(for: MagazinCoordinator.RutaMagazin.self) { ruta in
                    coordonator.construiesteVedere(pentruRuta: ruta)
                }
        }
    }
}

struct MagazinAcasaView: View {
    let coordonator: MagazinCoordinator

    var body: some View {
        List {
            Section("Categorii") {
                Button("Electronice") { coordonator.arataCategorie("Electronice") }
                Button("Îmbrăcăminte") { coordonator.arataCategorie("Îmbrăcăminte") }
            }
            Section("Acțiuni") {
                Button("Coșul Meu") { coordonator.navigheazaLaCos() }
            }
        }
        .navigationTitle("Magazin")
    }
}

struct DetaliiProdusCoordinatView: View {
    let idProdus: UUID
    let coordonator: MagazinCoordinator

    var body: some View {
        VStack(spacing: 20) {
            Text("Produs: \(idProdus.uuidString.prefix(8))")
                .font(.title)

            Button("Vezi Recenzii") {
                coordonator.arataRecenzii(pentruProdus: idProdus)
            }
            .buttonStyle(.borderedProminent)

            Button("Adaugă în Coș și Vezi Coșul") {
                coordonator.navigheazaLaCos()
            }
            .buttonStyle(.bordered)
        }
        .navigationTitle("Detalii Produs")
    }
}

Restaurarea Stării Navigării

De Ce Este Importantă Restaurarea Stării

Utilizatorii se așteaptă ca aplicația să revină exact unde au lăsat-o. Gândiți-vă: dacă un utilizator era pe pagina unui produs și aplicația este terminată de sistem (din cauza memoriei insuficiente, de exemplu), la redeschidere ar trebui să revadă acel produs. Vestea bună e că NavigationPath suportă Codable, ceea ce face restaurarea stării posibilă.

NavigationStore pentru Persistență

@Observable
class NavigationStore {
    private let cheieStocaj = "cale_navigare_salvata"
    var caleNavigare = NavigationPath()

    private var dateCaleSalvata: Data? {
        get { UserDefaults.standard.data(forKey: cheieStocaj) }
        set { UserDefaults.standard.set(newValue, forKey: cheieStocaj) }
    }

    func salveaza() {
        guard let reprezentare = caleNavigare.codable else {
            print("Nu s-a putut obține reprezentarea codabilă a căii")
            return
        }
        do {
            let date = try JSONEncoder().encode(reprezentare)
            dateCaleSalvata = date
            print("Calea de navigare a fost salvată cu succes (\(caleNavigare.count) elemente)")
        } catch {
            print("Eroare la salvarea căii: \(error)")
        }
    }

    func restaureaza() {
        guard let date = dateCaleSalvata else {
            print("Nu există cale salvată")
            return
        }
        do {
            let reprezentare = try JSONDecoder().decode(
                NavigationPath.CodableRepresentation.self,
                from: date
            )
            caleNavigare = NavigationPath(reprezentare)
            print("Calea de navigare a fost restaurată cu succes")
        } catch {
            print("Eroare la restaurarea căii: \(error)")
            dateCaleSalvata = nil
        }
    }

    func sterge() {
        dateCaleSalvata = nil
        if !caleNavigare.isEmpty {
            caleNavigare.removeLast(caleNavigare.count)
        }
    }
}

Integrarea cu ScenePhase și SceneStorage

Pentru o restaurare completă, putem folosi ScenePhase pentru a detecta când aplicația trece în fundal:

struct AppCuRestaurareaStariiView: View {
    @State private var magazin = NavigationStore()
    @Environment(\.scenePhase) private var fazaScenei

    var body: some View {
        NavigationStack(path: $magazin.caleNavigare) {
            AcasaRestorabila()
                .navigationDestination(for: RutaCodabila.self) { ruta in
                    ruta.construiesteVedere()
                }
        }
        .onChange(of: fazaScenei) { _, fazaNoua in
            switch fazaNoua {
            case .background, .inactive:
                magazin.salveaza()
            case .active:
                break
            @unknown default:
                break
            }
        }
        .onAppear {
            magazin.restaureaza()
        }
    }
}

enum RutaCodabila: Codable, Hashable {
    case produs(id: String)
    case categorie(nume: String)
    case profil
    case setari
    case comanda(id: String)

    @ViewBuilder
    func construiesteVedere() -> some View {
        switch self {
        case .produs(let id):
            Text("Produs restaurat: \(id)").font(.title)
        case .categorie(let nume):
            Text("Categorie restaurată: \(nume)").font(.title)
        case .profil:
            Text("Profil restaurat").font(.title)
        case .setari:
            Text("Setări restaurate").font(.title)
        case .comanda(let id):
            Text("Comanda restaurată: \(id)").font(.title)
        }
    }
}

Utilizarea SceneStorage pentru Persistență Simplificată

Dacă nu aveți nevoie de control total, o alternativă mai simplă pentru restaurarea stării este @SceneStorage, care funcționează automat cu ciclul de viață al scenei:

struct AppCuSceneStorage: View {
    @SceneStorage("cale_navigare") private var dateCale: Data?
    @State private var caleNavigare = NavigationPath()

    var body: some View {
        NavigationStack(path: $caleNavigare) {
            ContentListView()
                .navigationDestination(for: RutaCodabila.self) { ruta in
                    ruta.construiesteVedere()
                }
        }
        .onChange(of: caleNavigare) {
            salveazaCale()
        }
        .onAppear {
            restaureazaCale()
        }
    }

    private func salveazaCale() {
        guard let reprezentare = caleNavigare.codable else { return }
        dateCale = try? JSONEncoder().encode(reprezentare)
    }

    private func restaureazaCale() {
        guard let date = dateCale else { return }
        guard let reprezentare = try? JSONDecoder().decode(
            NavigationPath.CodableRepresentation.self,
            from: date
        ) else { return }
        caleNavigare = NavigationPath(reprezentare)
    }
}

Un aspect important de reținut (și o capcană în care am căzut odată): pentru ca restaurarea stării să funcționeze, toate tipurile folosite în NavigationPath trebuie să fie conforme atât cu Hashable, cât și cu Codable. Dacă vreun tip din stivă nu e Codable, proprietatea .codable a lui NavigationPath va returna nil.

Bune Practici și Capcane Frecvente

Practici Recomandate

  1. Folosiți enumerări pentru rute, nu stringuri

    Stringurile sunt predispuse la erori de scriere și nu oferă siguranță la compilare. Enumerările cu valori asociate sunt întotdeauna preferabile:

    // Evitați - fragil și predispus la erori
    cale.append("produs_\(id)")
    
    // Recomandabil - sigur și clar
    cale.append(Ruta.produs(id: idProdus))
  2. Păstrați vederile fără cunoștințe de navigare

    Vederile nu ar trebui să știe cum funcționează navigarea. Folosiți callbacks sau coordonatori — veți mulțumi mai târziu:

    // Mai puțin ideal - vederea cunoaște structura navigării
    struct ProdusView: View {
        @Binding var cale: NavigationPath
        var body: some View {
            Button("Cumpără") {
                cale.append(Ruta.cos)
                cale.append(Ruta.finalizare)
            }
        }
    }
    
    // Mai bine - vederea delegă navigarea
    struct ProdusView: View {
        let laApasareCumpara: () -> Void
        var body: some View {
            Button("Cumpără") { laApasareCumpara() }
        }
    }
  3. Gestionați stiva cu grijă

    Verificați întotdeauna dacă stiva nu este goală înainte de a elimina elemente. Altfel, crash garantat:

    func navigheazaInapoi(pasi: Int = 1) {
        let pasiReali = min(pasi, caleNavigare.count)
        guard pasiReali > 0 else { return }
        caleNavigare.removeLast(pasiReali)
    }
  4. Centralizați definițiile .navigationDestination

    Evitați duplicarea definițiilor de destinație. Plasați-le cât mai sus în ierarhia de vederi sau folosiți un ViewBuilder centralizat.

  5. Folosiți @Observable în loc de ObservableObject

    Pe iOS 17+, macrocomanda @Observable oferă performanță mai bună și un API mai simplu decât combinația ObservableObject + @Published. Vederile se actualizează doar când proprietățile efectiv accesate se schimbă, nu la orice modificare a obiectului.

Capcane Frecvente

  • Uitarea conformanței Hashable

    Valorile transmise prin NavigationLink(value:) trebuie să fie Hashable. Pentru structuri simple, adăugați conformanța automată. Pentru structuri complexe, implementați manual hash(into:) și ==.

  • Definirea .navigationDestination în locul greșit

    Asta e o greșeală clasică. Modificatorul .navigationDestination trebuie plasat pe o vedere din interiorul NavigationStack, nu pe NavigationStack în sine:

    // Greșit - plasat pe NavigationStack
    NavigationStack {
        ListaView()
    }
    .navigationDestination(for: Produs.self) { produs in
        DetaliiView(produs: produs)
    }
    
    // Corect - plasat în interiorul NavigationStack
    NavigationStack {
        ListaView()
            .navigationDestination(for: Produs.self) { produs in
                DetaliiView(produs: produs)
            }
    }
  • Conflicte între mai multe .navigationDestination pentru același tip

    Dacă definiți mai multe destinații pentru același tip de valoare, doar ultima va fi folosită. Asigurați-vă că fiecare tip are o singură destinație în ierarhia activă.

  • Pierderea stării la navigarea înapoi

    Când utilizatorul navighează înapoi, vederea destinație este distrusă. Dacă acea vedere avea stare locală (un formular completat parțial, de exemplu), starea se pierde. Folosiți un model de date extern (@Observable sau un Store) pentru a persista datele importante.

  • Probleme cu tipurile ne-Codable în NavigationPath

    Dacă folosiți NavigationPath și doriți restaurarea stării, toate tipurile adăugate în stivă trebuie să fie Codable. Dacă adăugați chiar și un singur tip ne-Codable, proprietatea .codable va returna nil și nu veți putea salva starea.

  • Manipularea stivei din thread-uri secundare

    Modificările la NavigationPath trebuie făcute pe thread-ul principal. Când lucrați cu operații asincrone, asigurați-vă că actualizările sunt pe MainActor:

    @Observable
    @MainActor
    class NavigareManager {
        var caleNavigare = NavigationPath()
    
        func navigheazaDupaIncarcare() async {
            let rezultat = await ServiciuDate.incarca()
            caleNavigare.append(Ruta.rezultat(rezultat))
        }
    }

Sfaturi pentru Testare

Coordinator Pattern facilitează enorm testarea navigării. Puteți verifica starea coordonatorului fără a rula interfața grafică — ceea ce accelerează semnificativ ciclul de dezvoltare:

import Testing

@Suite("Teste MagazinCoordinator")
struct MagazinCoordinatorTests {

    @Test("Navigarea la un produs adaugă ruta corectă pe stivă")
    func navigareLaProdus() {
        let coordonator = MagazinCoordinator()
        let idProdus = UUID()

        coordonator.arataProdus(idProdus)

        #expect(coordonator.caleNavigare.count == 1)
    }

    @Test("Navigarea la rădăcină golește stiva")
    func navigareLaRadacina() {
        let coordonator = MagazinCoordinator()

        coordonator.arataCategorie("Electronice")
        coordonator.arataProdus(UUID())
        #expect(coordonator.caleNavigare.count == 2)

        coordonator.navigheazaLaRadacina()
        #expect(coordonator.caleNavigare.isEmpty)
    }

    @Test("Deep link-ul la un produs resetează și navighează corect")
    func deepLinkProdus() {
        let coordonator = MagazinCoordinator()
        let uuid = UUID()

        coordonator.arataCategorie("Haine")
        #expect(coordonator.caleNavigare.count == 1)

        coordonator.proceseazaDeepLink(.produs(id: uuid.uuidString))

        #expect(coordonator.caleNavigare.count == 1)
    }
}

Performanță și Optimizare

Câteva considerații de performanță pe care merită să le aveți în vedere:

  • Evitați stive foarte adânci — fiecare vedere din stivă consumă memorie. Dacă aveți un flux care ar putea genera zeci de vederi pe stivă, luați în considerare resetarea periodică.
  • Folosiți lazy loading pentru destinații — vederile destinație sunt create doar când sunt necesare, dar datele lor pot fi preîncărcate. Evitați încărcarea datelor grele în inițializatorul vederii; folosiți în schimb .task sau .onAppear.
  • Nu stocați obiecte mari în rute — rutele ar trebui să conțină doar identificatori (ID-uri, stringuri), nu obiecte complete. Vederea destinație poate apoi să încarce datele complete folosind identificatorul.
// Evitați - obiect mare în rută
enum RutaIneficienta: Hashable {
    case produs(Produs) // Produs poate conține imagini, descrieri lungi etc.
}

// Recomandat - doar identificatorul în rută
enum RutaEficienta: Hashable {
    case produs(id: UUID) // Vederea va încărca datele produsului
}

struct ProdusEficientView: View {
    let idProdus: UUID
    @State private var produs: Produs?

    var body: some View {
        Group {
            if let produs {
                DetaliiCompleteProdusView(produs: produs)
            } else {
                ProgressView("Se încarcă...")
            }
        }
        .task {
            produs = await ServiciuProduse.shared.incarca(id: idProdus)
        }
    }
}

Concluzie

NavigationStack reprezintă un salt major în evoluția navigării în SwiftUI. Față de predecesorul său NavigationView, oferă control programatic complet, suport nativ pentru deep linking, și posibilitatea restaurării stării — toate într-un API declarativ și elegant.

Am parcurs în acest ghid întregul spectru al navigării moderne în SwiftUI:

  • FundamenteleNavigationStack, NavigationLink cu valori, și .navigationDestination
  • NavigationPath — stiva type-erased cu operații programatice de push și pop
  • Rute bazate pe enumerări — navigare type-safe care elimină erorile la rulare
  • Navigare programatică — controlul complet al fluxului prin manipularea stării
  • Deep linking — conectarea URL-urilor externe cu ecranele aplicației
  • Coordinator Pattern — separarea logicii de navigare de vederi pentru scalabilitate și testabilitate
  • Restaurarea stării — persistența navigării între sesiuni ale aplicației

Alegerea abordării potrivite depinde de complexitatea aplicației voastre. Pentru aplicații simple, NavigationStack cu rute bazate pe enumerări e mai mult decât suficient. Pentru aplicații de producție cu echipe mari, Coordinator Pattern vă oferă structura și testabilitatea de care aveți nevoie.

Indiferent de complexitatea proiectului, principiile rămân aceleași: mențineți navigarea bazată pe stare, separați logica de navigare de vederi, și folosiți tipuri sigure pentru rute. Cu aceste principii ca fundament, veți construi aplicații cu navigare robustă, ușor de întreținut și plăcută pentru utilizatori.

Sper că acest ghid v-a fost de folos. Explorați aceste concepte în propriile proiecte, experimentați cu diferite combinații, și nu ezitați să adaptați pattern-urile prezentate la nevoile specifice ale aplicației voastre. Navigarea modernă în SwiftUI nu mai este o provocare — e o oportunitate.

Despre Autor Editorial Team

Our team of expert writers and editors.