Navigácia v SwiftUI od A po Z: NavigationStack, deep linking a Router pattern v praxi

Kompletný sprievodca navigáciou v SwiftUI — od NavigationStack a NavigationPath, cez programatickú navigáciu a deep linking, až po Router a Coordinator pattern s praktickými príkladmi.

Úvod: Prečo je navigácia v SwiftUI taká dôležitá

Navigácia je chrbtovou kosťou každej mobilnej aplikácie. Môžete mať perfektný dizajn, bezchybný dátový model a najrýchlejšiu sieťovú vrstvu na svete — ale ak sa používateľ nevie jednoducho dostať tam, kam potrebuje, vaša aplikácia jednoducho zlyháva. Bodka.

A v SwiftUI prešla navigácia za posledné roky naozaj dramatickým vývojom. Pamätáte si ešte NavigationView? Ten prvý pokus, ktorý síce fungoval — ale mal toľko limitácií, že vývojári končili s hackami a workaroundmi na každom kroku. Programatická navigácia? Nočná mora. Deep linking? Na to radšej zabudnite.

Potom na WWDC 2022 prišiel NavigationStack a všetko sa zmenilo. Namiesto implicitného prepojenia cez NavigationLink s priamym cieľovým view sme dostali value-based navigáciu, NavigationPath pre typovo bezpečné riadenie zásobníka a plnú kontrolu nad navigačnou históriou. Úprimne, bol to asi najväčší kvalitatívny skok v celom SwiftUI frameworku.

V tomto článku si prejdeme celý navigačný ekosystém SwiftUI — od základov NavigationStack, cez pokročilú programatickú navigáciu a deep linking, až po architektonické vzory ako Router a Coordinator pattern. Každú sekciu doplníme praktickými príkladmi, ktoré môžete priamo použiť vo svojich projektoch. Tak poďme na to.

Od NavigationView k NavigationStack: Evolúcia navigácie

Aby sme pochopili, prečo je NavigationStack taký výrazný krok vpred, pozrime sa najprv na to, ako to fungovalo predtým.

Starý spôsob: NavigationView

// iOS 13-15 prístup — dnes deprecated
struct StaryPristupView: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink("Detail článku", destination: DetailView(id: 1))
                NavigationLink("Nastavenia", destination: NastaveniaView())
            }
            .navigationTitle("Domov")
        }
    }
}

Vidíte ten problém? Cieľový view (destination) sa vytvoril okamžite — nie keď naň používateľ ťukol, ale pri renderovaní zoznamu. Pri zozname s 1000 položkami to znamenalo 1000 inicializácií views. Tisíc. To je proste neefektívne.

Programatická navigácia vyžadovala prácu s isActive binding a tag/selection modifikátormi, čo rýchlo viedlo k neprehľadnému kódu. A deep linking? Na to ste potrebovali externú knižnicu alebo veľmi kreatívny workaround (a veľa trpezlivosti).

Moderný spôsob: NavigationStack

// iOS 16+ prístup
struct ModernyPristupView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Detail článku", value: Clanok(id: 1, nazov: "SwiftUI"))
                NavigationLink("Nastavenia", value: Destinacia.nastavenia)
            }
            .navigationTitle("Domov")
            .navigationDestination(for: Clanok.self) { clanok in
                DetailClanku(clanok: clanok)
            }
            .navigationDestination(for: Destinacia.self) { destinacia in
                switch destinacia {
                case .nastavenia:
                    NastaveniaView()
                case .profil:
                    ProfilView()
                }
            }
        }
    }
}

Vidíte ten rozdiel? Cieľový view sa vytvára až keď je skutočne potrebný — prostredníctvom .navigationDestination(for:) modifikátora. NavigationLink teraz prenáša iba hodnotu (value), nie celý view. Je to čistejšie, efektívnejšie a — čo je podľa mňa najdôležitejšie — programaticky ovládateľné.

NavigationStack do hĺbky: Zásobníková navigácia v praxi

NavigationStack funguje na princípe zásobníka (stack) — presne ako zásobník tanierov v jedálni. Každý nový view, na ktorý navigujete, sa položí na vrch. Keď sa vrátite späť, view sa z vrchu odoberie. Jednoduchý a predvídateľný model, s ktorým sa pracuje príjemne.

Základná konfigurácia s explicitnou cestou

struct HlavnyView: View {
    @State private var navigacnaCesta: [String] = []

    var body: some View {
        NavigationStack(path: $navigacnaCesta) {
            VStack(spacing: 20) {
                Button("Prejsť na A") {
                    navigacnaCesta.append("A")
                }
                Button("Prejsť na B") {
                    navigacnaCesta.append("B")
                }
                Button("Prejsť priamo na A → B → C") {
                    navigacnaCesta = ["A", "B", "C"]
                }
            }
            .navigationTitle("Domov")
            .navigationDestination(for: String.self) { hodnota in
                Text("Obrazovka \(hodnota)")
                    .navigationTitle("Obrazovka \(hodnota)")
            }
        }
    }
}

Všimnite si ten tretí button — jedným priradením do navigacnaCesta vytvoríte celý zásobník navigácie naraz. Používateľ sa ocitne na obrazovke C, s možnosťou vrátiť sa cez B na A a potom domov. Toto je presne tá mágia, ktorá robí NavigationStack tak mocným nástrojom.

Návrat na root view

Častý požiadavok v aplikáciách je tlačidlo „Späť na domov", ktoré preskočí všetky medzikroky. S NavigationStack je to až smiešne triviálne:

struct DetailView: View {
    @Binding var navigacnaCesta: [String]

    var body: some View {
        VStack {
            Text("Ste hlboko v navigácii")
            Button("Späť na domov") {
                navigacnaCesta.removeAll()
            }
        }
    }
}

Jednoduché removeAll() vyprázdni zásobník a SwiftUI automaticky animuje návrat na root view. Žiadne hacky, žiadne popToRootViewController ako v UIKit. Kto si pamätá tie časy, vie o čom hovorím.

NavigationPath: Typovo bezpečná navigácia s rôznymi typmi

V predchádzajúcich príkladoch sme používali pole jedného typu ([String]). Ale čo ak vaša aplikácia naviguje na rôzne typy obrazoviek — články, kategórie, profily používateľov? Tu prichádza na scénu NavigationPath.

// Definícia modelov
struct Clanok: Hashable {
    let id: Int
    let nazov: String
}

struct Kategoria: Hashable {
    let id: Int
    let meno: String
}

struct ProfilPouzivatela: Hashable {
    let uzivatelId: Int
    let menoUzivatela: String
}

// Použitie NavigationPath
struct AplikaciaSNavigacnouCestou: View {
    @State private var cesta = NavigationPath()

    var body: some View {
        NavigationStack(path: $cesta) {
            VStack(spacing: 16) {
                Button("Otvoriť článok") {
                    cesta.append(Clanok(id: 1, nazov: "SwiftUI navigácia"))
                }
                Button("Otvoriť kategóriu") {
                    cesta.append(Kategoria(id: 5, meno: "Tutoriály"))
                }
                Button("Otvoriť profil") {
                    cesta.append(ProfilPouzivatela(uzivatelId: 42, menoUzivatela: "janko"))
                }
            }
            .navigationTitle("Domov")
            .navigationDestination(for: Clanok.self) { clanok in
                Text("Článok: \(clanok.nazov)")
            }
            .navigationDestination(for: Kategoria.self) { kategoria in
                Text("Kategória: \(kategoria.meno)")
            }
            .navigationDestination(for: ProfilPouzivatela.self) { profil in
                Text("Profil: \(profil.menoUzivatela)")
            }
        }
    }
}

NavigationPath je type-erased kolekcia, ktorá si interne uchováva typové informácie. Môžete do nej vkladať hodnoty rôznych typov — jedinou podmienkou je, že musia byť Hashable. SwiftUI potom na základe typu hodnoty automaticky vyberie správny .navigationDestination(for:) modifikátor. Elegantné, nie?

Serializácia NavigationPath pre obnovenie stavu

Tu je jedna bonusová vlastnosť, ktorá sa mi osobne veľmi páči — NavigationPath podporuje kódovanie a dekódovanie. To znamená, že navigačný stav môžete uložiť a obnoviť, napríklad keď systém ukončí aplikáciu na pozadí:

class NavigacnyStav: ObservableObject {
    @Published var cesta = NavigationPath()

    // Uloženie stavu
    func ulozitStav() {
        guard let reprezentacia = cesta.codable else { return }
        let encoder = JSONEncoder()
        if let data = try? encoder.encode(reprezentacia) {
            UserDefaults.standard.set(data, forKey: "navigacnaCesta")
        }
    }

    // Obnovenie stavu
    func obnovitStav() {
        guard let data = UserDefaults.standard.data(forKey: "navigacnaCesta"),
              let reprezentacia = try? JSONDecoder().decode(
                  NavigationPath.CodableRepresentation.self, from: data
              ) else { return }
        cesta = NavigationPath(reprezentacia)
    }
}

Táto funkcia je nesmierne užitočná pre aplikácie, kde chcete zachovať presný navigačný stav používateľa — čítačky článkov, e-commerce appky alebo komplexné dashboard rozhrania. Len nezabudnite, že všetky typy vo vašom NavigationPath musia byť Codable aj Hashable.

Programatická navigácia: Plná kontrola v kóde

Programatická navigácia je situácia, keď váš kód rozhoduje, kam navigovať — nie používateľ priamym ťuknutím na link. Toto je podľa mňa jedna z oblastí, kde NavigationStack skutočne žiari. Typické scenáre zahŕňajú:

  • Navigácia po úspešnom prihlásení
  • Presmerovanie po dokončení nákupu
  • Spracovanie push notifikácie
  • Presmerovanie na základe používateľskej role
  • Onboarding flow s podmienenými krokmi

Príklad: Prihlasovací flow

enum PrihlasovaciaObrazovka: Hashable {
    case zadanieEmailu
    case zadanieHesla(email: String)
    case verifikacia(email: String)
    case hlavnaObrazovka
}

struct PrihlasovaciFlov: View {
    @State private var cesta = NavigationPath()

    var body: some View {
        NavigationStack(path: $cesta) {
            UvodnaObrazovka(cesta: $cesta)
                .navigationDestination(for: PrihlasovaciaObrazovka.self) { obrazovka in
                    switch obrazovka {
                    case .zadanieEmailu:
                        EmailView(cesta: $cesta)
                    case .zadanieHesla(let email):
                        HesloView(email: email, cesta: $cesta)
                    case .verifikacia(let email):
                        VerifikaciaView(email: email, cesta: $cesta)
                    case .hlavnaObrazovka:
                        HlavnaView()
                    }
                }
        }
    }
}

struct EmailView: View {
    @State private var email = ""
    @Binding var cesta: NavigationPath

    var body: some View {
        VStack(spacing: 20) {
            TextField("Váš email", text: $email)
                .textFieldStyle(.roundedBorder)
                .keyboardType(.emailAddress)
                .autocapitalization(.none)

            Button("Pokračovať") {
                // Validácia emailu
                if email.contains("@") {
                    cesta.append(PrihlasovaciaObrazovka.zadanieHesla(email: email))
                }
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .navigationTitle("Zadajte email")
    }
}

struct HesloView: View {
    let email: String
    @State private var heslo = ""
    @Binding var cesta: NavigationPath

    var body: some View {
        VStack(spacing: 20) {
            SecureField("Vaše heslo", text: $heslo)
                .textFieldStyle(.roundedBorder)

            Button("Prihlásiť sa") {
                Task {
                    let uspech = await prihlasit(email: email, heslo: heslo)
                    if uspech {
                        // Po úspešnom prihlásení → rovno na hlavnú obrazovku
                        cesta = NavigationPath()
                        cesta.append(PrihlasovaciaObrazovka.hlavnaObrazovka)
                    }
                }
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .navigationTitle("Zadajte heslo")
    }

    func prihlasit(email: String, heslo: String) async -> Bool {
        // Simulácia sieťového volania
        try? await Task.sleep(for: .seconds(1))
        return true
    }
}

Tento príklad demonštruje niekoľko kľúčových konceptov. Enum PrihlasovaciaObrazovka s asociovanými hodnotami slúži ako definícia všetkých možných obrazoviek. Navigácia je riadená výlučne kódom — po validácii emailu sa automaticky prejde na heslo, po úspešnom prihlásení na hlavnú obrazovku.

A ten switch v .navigationDestination? To je podľa mňa najelegantnejší spôsob, ako centralizovať vytváranie views v celom SwiftUI.

Deep linking: Otvorenie správnej obrazovky z URL

Deep linking je schopnosť aplikácie otvoriť konkrétnu obrazovku na základe URL adresy. Ak ste s tým ešte nepracovali, pripravte sa — je to jednoduchšie, než by ste čakali. Existujú dva hlavné typy:

  • URL schéma (napr. mojaaplikacia://clanok/123) — funguje iba ak je aplikácia nainštalovaná
  • Universal Links (napr. https://mojaapp.sk/clanok/123) — ak nie je aplikácia nainštalovaná, otvorí webovú stránku

Spracovanie deep linkov s NavigationStack

// Centralizovaný parser deep linkov
struct DeepLinkParser {
    enum Destinacia: Hashable {
        case clanok(id: Int)
        case kategoria(slug: String)
        case profil(uzivatelId: Int)
        case nastavenia
    }

    static func spracovat(url: URL) -> Destinacia? {
        guard let komponenty = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            return nil
        }

        let casti = komponenty.path
            .split(separator: "/")
            .map(String.init)

        switch casti.first {
        case "clanok":
            if let idString = casti.dropFirst().first,
               let id = Int(idString) {
                return .clanok(id: id)
            }
        case "kategoria":
            if let slug = casti.dropFirst().first {
                return .kategoria(slug: slug)
            }
        case "profil":
            if let idString = casti.dropFirst().first,
               let id = Int(idString) {
                return .profil(uzivatelId: id)
            }
        case "nastavenia":
            return .nastavenia
        default:
            break
        }

        return nil
    }
}

// Hlavná aplikácia s deep link podporou
@main
struct MojaAplikacia: App {
    @State private var navigacnaCesta = NavigationPath()

    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $navigacnaCesta) {
                DomovView()
                    .navigationDestination(for: DeepLinkParser.Destinacia.self) { dest in
                        switch dest {
                        case .clanok(let id):
                            DetailClanku(clanokId: id)
                        case .kategoria(let slug):
                            KategoriaView(slug: slug)
                        case .profil(let uzivatelId):
                            ProfilView(uzivatelId: uzivatelId)
                        case .nastavenia:
                            NastaveniaView()
                        }
                    }
            }
            .onOpenURL { url in
                spracujDeepLink(url: url)
            }
        }
    }

    private func spracujDeepLink(url: URL) {
        guard let destinacia = DeepLinkParser.spracovat(url: url) else { return }

        // Resetovať navigáciu a prejsť na cieľovú obrazovku
        navigacnaCesta = NavigationPath()

        // Krátke oneskorenie pre clean transition
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            navigacnaCesta.append(destinacia)
        }
    }
}

Kľúčový princíp je tu centralizácia. Všetky deep linky — či už prídu z URL schémy, universal linku, push notifikácie alebo widgetu — prechádzajú jedným parserom a jedným navigačným mechanizmom. Žiadna duplikácia logiky, jednoduchá údržba. Verím, že toto ocení každý, kto niekedy ladil deep linking s roztrúsenou logikou po celej appke.

Spracovanie deep linkov pri studenom štarte

Tu je jeden dôležitý detail, na ktorý sa ľahko zabúda. Keď aplikácia nie je v pamäti a deep link ju spúšťa „za studena", .onOpenURL sa zavolá ešte pred tým, ako je navigačný zásobník plne inicializovaný. Riešenie je našťastie jednoduché — uložíme si čakajúci deep link a spracujeme ho, keď je aplikácia pripravená:

@Observable
class DeepLinkManager {
    var cakajuciDeepLink: DeepLinkParser.Destinacia?
    var jeAplikaciaPripravena = false

    func spracujURL(_ url: URL) {
        guard let destinacia = DeepLinkParser.spracovat(url: url) else { return }

        if jeAplikaciaPripravena {
            navigovatNa(destinacia)
        } else {
            cakajuciDeepLink = destinacia
        }
    }

    func aplikaciaJePripravena(cesta: Binding<NavigationPath>) {
        jeAplikaciaPripravena = true
        if let cakajuci = cakajuciDeepLink {
            navigovatNa(cakajuci)
            cakajuciDeepLink = nil
        }
    }

    private func navigovatNa(_ destinacia: DeepLinkParser.Destinacia) {
        // Navigačná logika
        NotificationCenter.default.post(
            name: .deepLinkNavigation,
            object: destinacia
        )
    }
}

extension Notification.Name {
    static let deepLinkNavigation = Notification.Name("deepLinkNavigation")
}

Router pattern: Škálovateľná architektúra navigácie

Pre menšie aplikácie s 5-10 obrazovkami je priame používanie NavigationPath úplne v poriadku. Ale akonáhle vaša aplikácia narastie na desiatky obrazoviek s rôznymi navigačnými zásobníkmi a modálnymi prezentáciami, potrebujete niečo robustnejšie. Tu nastupuje Router pattern — a osobne ho považujem za sweet spot medzi jednoduchosťou a škálovateľnosťou.

Definícia route enumu

// Všetky možné cieľové obrazovky definované na jednom mieste
enum AppRoute: Hashable {
    // Články
    case zoznamClankov
    case detailClanku(id: Int)
    case editorClanku(id: Int?)

    // Používateľ
    case profil(uzivatelId: Int)
    case upravitProfil

    // Nastavenia
    case nastavenia
    case oAplikacii
    case notifikacie

    // E-commerce
    case produktDetail(productId: String)
    case kosik
    case pokladna
    case potvrdenie(objednavkaId: String)
}

Router trieda

@Observable
class AppRouter {
    var cesta = NavigationPath()
    var modalnySheet: AppRoute?
    var celoplostnyModal: AppRoute?
    var zobrazitAlert = false
    var alertSprava = ""

    // MARK: - Push navigácia

    func navigovatNa(_ route: AppRoute) {
        cesta.append(route)
    }

    func navigovatNaViacerych(_ routes: [AppRoute]) {
        for route in routes {
            cesta.append(route)
        }
    }

    // MARK: - Pop navigácia

    func spatNaRoot() {
        cesta = NavigationPath()
    }

    func spat() {
        guard !cesta.isEmpty else { return }
        cesta.removeLast()
    }

    func spat(o pocet: Int) {
        let skutocnyPocet = min(pocet, cesta.count)
        cesta.removeLast(skutocnyPocet)
    }

    // MARK: - Modálna prezentácia

    func zobrazitSheet(_ route: AppRoute) {
        modalnySheet = route
    }

    func zobrazitCeloplostny(_ route: AppRoute) {
        celoplostnyModal = route
    }

    func zavrietModal() {
        modalnySheet = nil
        celoplostnyModal = nil
    }

    // MARK: - Deep link spracovanie

    func spracujDeepLink(_ url: URL) {
        guard let destinacia = DeepLinkParser.spracovat(url: url) else { return }

        spatNaRoot()

        switch destinacia {
        case .clanok(let id):
            navigovatNa(.detailClanku(id: id))
        case .kategoria(let slug):
            navigovatNa(.zoznamClankov)
        case .profil(let uzivatelId):
            navigovatNa(.profil(uzivatelId: uzivatelId))
        case .nastavenia:
            navigovatNa(.nastavenia)
        }
    }
}

Integrácia s view hierarchiou

struct KorenovyView: View {
    @State private var router = AppRouter()

    var body: some View {
        NavigationStack(path: $router.cesta) {
            DomovView()
                .navigationDestination(for: AppRoute.self) { route in
                    vytvoritView(pre: route)
                }
        }
        .sheet(item: $router.modalnySheet) { route in
            vytvoritView(pre: route)
        }
        .fullScreenCover(item: $router.celoplostnyModal) { route in
            vytvoritView(pre: route)
        }
        .environment(router)
        .onOpenURL { url in
            router.spracujDeepLink(url)
        }
    }

    @ViewBuilder
    private func vytvoritView(pre route: AppRoute) -> some View {
        switch route {
        case .zoznamClankov:
            ZoznamClankovView()
        case .detailClanku(let id):
            DetailClankuView(clanokId: id)
        case .editorClanku(let id):
            EditorClankuView(clanokId: id)
        case .profil(let uzivatelId):
            ProfilView(uzivatelId: uzivatelId)
        case .upravitProfil:
            UpravitProfilView()
        case .nastavenia:
            NastaveniaView()
        case .oAplikacii:
            OAplikaciiView()
        case .notifikacie:
            NotifikacieView()
        case .produktDetail(let productId):
            ProduktDetailView(productId: productId)
        case .kosik:
            KosikView()
        case .pokladna:
            PokladnaView()
        case .potvrdenie(let objednavkaId):
            PotvrdeniObjednavkyView(objednavkaId: objednavkaId)
        }
    }
}

// Aby AppRoute fungoval ako identifiable pre sheety
extension AppRoute: Identifiable {
    var id: String {
        String(describing: self)
    }
}

Použitie routera v podradených views

struct DetailClankuView: View {
    let clanokId: Int
    @Environment(AppRouter.self) private var router

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                Text("Článok #\(clanokId)")
                    .font(.largeTitle)

                Text("Tu je obsah článku...")
                    .font(.body)

                // Navigácia na súvisiaci obsah
                Button("Zobraziť profil autora") {
                    router.navigovatNa(.profil(uzivatelId: 42))
                }

                Button("Upraviť článok") {
                    router.zobrazitSheet(.editorClanku(id: clanokId))
                }

                Button("Späť na zoznam") {
                    router.spatNaRoot()
                    router.navigovatNa(.zoznamClankov)
                }
            }
            .padding()
        }
        .navigationTitle("Detail článku")
    }
}

Výhody Router patternu sú jasné: navigačná logika je centralizovaná na jednom mieste, views sa nestarajú o to ako navigovať (len kam), deep linking sa integruje priamo do routera a testovanie je jednoduché. Stačí vytvoriť router, zavolať jeho metódy a overiť, že cesta obsahuje správne hodnoty. Nič viac.

Coordinator pattern: Pre naozaj veľké aplikácie

Router pattern je skvelý, ale pri naozaj veľkých aplikáciách s viacerými navigačnými zásobníkmi (napríklad TabView s NavigationStack v každom tabe) sa oplatí ísť ešte o krok ďalej — ku Coordinator patternu. Koordinátor riadi navigačný tok pre konkrétnu feature alebo celý modul aplikácie.

Musím priznať, že nie každá aplikácia tento level abstrakcie potrebuje. Ale ak pracujete v tíme s 5+ vývojármi na appke s desiatkami modulov, coordinator vám ušetrí veľa bolesti hlavy.

// Protokol pre všetkých koordinátorov
protocol Koordinator: AnyObject {
    var navigacnaCesta: NavigationPath { get set }
    func start()
}

// Koordinátor pre článkový modul
@Observable
class ClankovyKoordinator: Koordinator {
    var navigacnaCesta = NavigationPath()
    var aktualnySheet: ClankovaRoute?

    enum ClankovaRoute: Hashable, Identifiable {
        case zoznam
        case detail(id: Int)
        case editor(id: Int?)
        case zdielatClanok(id: Int)

        var id: String { String(describing: self) }
    }

    func start() {
        navigacnaCesta = NavigationPath()
    }

    func zobrazitDetail(clanokId: Int) {
        navigacnaCesta.append(ClankovaRoute.detail(id: clanokId))
    }

    func vytvorirNovyClanok() {
        aktualnySheet = .editor(id: nil)
    }

    func upravitClanok(id: Int) {
        aktualnySheet = .editor(id: id)
    }

    func zdielatClanok(id: Int) {
        aktualnySheet = .zdielatClanok(id: id)
    }

    func spatNaZoznam() {
        navigacnaCesta = NavigationPath()
    }
}

// Použitie v TabView
struct HlavnaTabView: View {
    @State private var clankovyKoordinator = ClankovyKoordinator()
    @State private var profilovyKoordinator = ProfilovyKoordinator()

    var body: some View {
        TabView {
            Tab("Články", systemImage: "doc.text") {
                ClankovyFlow(koordinator: clankovyKoordinator)
            }

            Tab("Profil", systemImage: "person") {
                ProfilovyFlow(koordinator: profilovyKoordinator)
            }
        }
    }
}

struct ClankovyFlow: View {
    @Bindable var koordinator: ClankovyKoordinator

    var body: some View {
        NavigationStack(path: $koordinator.navigacnaCesta) {
            ZoznamClankovView()
                .navigationDestination(for: ClankovyKoordinator.ClankovaRoute.self) { route in
                    switch route {
                    case .zoznam:
                        ZoznamClankovView()
                    case .detail(let id):
                        DetailClankuView(clanokId: id)
                    case .editor(let id):
                        EditorClankuView(clanokId: id)
                    case .zdielatClanok(let id):
                        ZdielanieView(clanokId: id)
                    }
                }
        }
        .sheet(item: $koordinator.aktualnySheet) { route in
            switch route {
            case .editor(let id):
                EditorClankuView(clanokId: id)
            case .zdielatClanok(let id):
                ZdielanieView(clanokId: id)
            default:
                EmptyView()
            }
        }
        .environment(koordinator)
    }
}

Každý tab má vlastný koordinátor, vlastný navigačný zásobník a vlastnú logiku. Moduly sú od seba izolované, čo uľahčuje testovanie, refaktorovanie a prácu vo väčšom tíme.

Modálna navigácia: Sheet, fullScreenCover a confirmationDialog

Nie všetka navigácia je push/pop na zásobníku. Niekedy jednoducho potrebujete modálnu prezentáciu — formuláre, dialógy, výberové obrazovky. SwiftUI ponúka tri hlavné modálne mechanizmy a každý má svoje miesto:

struct ModalnaNavigaciaView: View {
    @State private var zobrazitSheet = false
    @State private var zobrazitCeloplostny = false
    @State private var zobrazitDialog = false
    @State private var vybranyItem: PolozkaMenu?

    var body: some View {
        VStack(spacing: 20) {
            Button("Otvoriť sheet") {
                zobrazitSheet = true
            }

            Button("Celoplošný modal") {
                zobrazitCeloplostny = true
            }

            Button("Potvrdzovací dialóg") {
                zobrazitDialog = true
            }

            // Item-based sheet — bezpečnejší prístup
            Button("Sheet s položkou") {
                vybranyItem = PolozkaMenu(nazov: "Nastavenia", ikona: "gear")
            }
        }
        .sheet(isPresented: $zobrazitSheet) {
            FormularView()
                .presentationDetents([.medium, .large])
                .presentationDragIndicator(.visible)
        }
        .fullScreenCover(isPresented: $zobrazitCeloplostny) {
            OnboardingView()
        }
        .confirmationDialog("Vyberte akciu", isPresented: $zobrazitDialog) {
            Button("Upraviť") { /* ... */ }
            Button("Zdieľať") { /* ... */ }
            Button("Vymazať", role: .destructive) { /* ... */ }
        }
        .sheet(item: $vybranyItem) { polozka in
            DetailPolozkyView(polozka: polozka)
        }
    }
}

struct PolozkaMenu: Identifiable {
    let id = UUID()
    let nazov: String
    let ikona: String
}

Malý tip na záver tejto sekcie: preferujte .sheet(item:) pred .sheet(isPresented:) keď prezentujete obsah závislý od konkrétnych dát. Je to typovo bezpečnejšie a predchádza situáciám, kde sheet sa otvorí bez správnych dát. Urobil som túto chybu viackrát, než som sa poučil.

NavigationSplitView: Navigácia pre iPad a Mac

Pre iPad a macOS aplikácie ponúka SwiftUI NavigationSplitView, ktorý zobrazuje obsah v dvoch alebo troch stĺpcoch — sidebar, content a detail:

struct AdaptivnaNavigacia: View {
    @State private var vybranaKategoria: Kategoria?
    @State private var vybranyClanok: Clanok?

    let kategorie = [
        Kategoria(id: 1, meno: "SwiftUI"),
        Kategoria(id: 2, meno: "Swift"),
        Kategoria(id: 3, meno: "UIKit")
    ]

    var body: some View {
        NavigationSplitView {
            // Sidebar
            List(kategorie, selection: $vybranaKategoria) { kategoria in
                NavigationLink(value: kategoria) {
                    Label(kategoria.meno, systemImage: "folder")
                }
            }
            .navigationTitle("Kategórie")
        } content: {
            // Stredný stĺpec
            if let kategoria = vybranaKategoria {
                ZoznamClankovKategorie(
                    kategoria: kategoria,
                    vyber: $vybranyClanok
                )
            } else {
                ContentUnavailableView(
                    "Vyberte kategóriu",
                    systemImage: "sidebar.left",
                    description: Text("Vyberte kategóriu zo zoznamu vľavo")
                )
            }
        } detail: {
            // Detail
            if let clanok = vybranyClanok {
                DetailClankuView(clanokId: clanok.id)
            } else {
                ContentUnavailableView(
                    "Vyberte článok",
                    systemImage: "doc.text",
                    description: Text("Vyberte článok pre zobrazenie detailu")
                )
            }
        }
        .navigationSplitViewStyle(.balanced)
    }
}

Na iPhone sa NavigationSplitView automaticky sbalí do zásobníkovej navigácie. Na iPade a Macu zobrazí stĺpce vedľa seba. Jedno view, dva rôzne navigačné modely — to je tá krása deklaratívneho prístupu SwiftUI.

Novinky z iOS 26 a WWDC 2025

Na WWDC 2025 Apple predstavil iOS 26 s novým dizajnovým jazykom Liquid Glass, ktorý ovplyvňuje aj navigačné prvky. Poďme sa pozrieť, čo to znamená pre vašu navigáciu.

Automatický nový vzhľad

Toto je fajn správa: keď skompilujete svoju existujúcu aplikáciu s Xcode 26, navigačné bary, toolbary a tab bary automaticky získajú nový sklenený vzhľad. Nie je potrebná žiadna zmena kódu — vďaka deklaratívnej povahe SwiftUI sa dizajn aktualizuje sám. Proste to funguje.

Vylepšenia Tab navigácie

iOS 26 priniesol nové API pre tab navigáciu s podporou rolí. Napríklad rola .search umožňuje umiestniť vyhľadávanie samostatne v spodnej časti obrazovky:

TabView {
    Tab("Domov", systemImage: "house") {
        NavigationStack {
            DomovView()
        }
    }

    Tab("Články", systemImage: "doc.text") {
        NavigationStack {
            ZoznamClankovView()
        }
    }

    // Nová search rola v iOS 26
    Tab("Hľadať", systemImage: "magnifyingglass", role: .search) {
        NavigationStack {
            VyhladavanieView()
        }
    }
}

ToolbarSpacer

Nový typ ToolbarSpacer umožňuje rozdeliť toolbar položky do skupín s vizuálnymi medzerami:

.toolbar {
    ToolbarItem(placement: .primaryAction) {
        Button("Uložiť", systemImage: "square.and.arrow.down") { }
    }

    ToolbarSpacer(.fixed)

    ToolbarItem(placement: .primaryAction) {
        Button("Zdieľať", systemImage: "square.and.arrow.up") { }
    }
}

Vylepšené vyhľadávanie

Nový modifikátor searchToolbarBehavior() umožňuje minimalizovať vyhľadávacie pole do toolbar tlačidla, čo šetrí miesto na obrazovke:

NavigationStack {
    ZoznamView()
        .searchable(text: $hladanyVyraz)
        .searchToolbarBehavior(.minimize)
}

Praktické tipy a bežné chyby

Na záver si zhrnieme najdôležitejšie praktické rady. Toto sú veci, ktoré vám ušetria hodiny (niekedy aj dni) ladenia.

1. Nezabúdajte na Hashable

Všetky typy používané v NavigationLink(value:) a .navigationDestination(for:) musia byť Hashable. Pre jednoduché štruktúry Swift automaticky syntetizuje conformance, ale pre triedy musíte implementovať hash(into:) a == manuálne.

2. Jeden navigationDestination pre každý typ

Nikdy nepoužívajte dva .navigationDestination(for: SameType.self) v rovnakom view hierarchii. SwiftUI použije iba jeden z nich a výsledok je nepredvídateľný. Ak potrebujete viac typov, použite buď NavigationPath, alebo wrapper enum. Toto je jedno z tých pravidiel, ktoré keď porušíte, budete dlho hľadať bug.

3. Vyhýbajte sa NavigationStack vnútri NavigationStack

Vnorené NavigationStack sú zdrojom podivných bugov — dvojité navigačné bary, nefunkčné tlačidlo späť, strata navigačného stavu. Jednoducho sa im vyhnite:

// ❌ Zlý prístup
NavigationStack {
    TabView {
        NavigationStack { // Vnorený — problém!
            ZoznamView()
        }
    }
}

// ✅ Správny prístup
TabView {
    NavigationStack {
        ZoznamView()
    }
    NavigationStack {
        ProfilView()
    }
}

4. Pozor na umiestnenie navigationDestination

Modifikátor .navigationDestination(for:) by mal byť umiestnený na view vnútri NavigationStack, nie na samotnom NavigationStack. Drobný detail, ale vie poriadne potrápiť:

// ❌ Zlý prístup
NavigationStack(path: $cesta)
    .navigationDestination(for: Route.self) { route in ... }

// ✅ Správny prístup
NavigationStack(path: $cesta) {
    RootView()
        .navigationDestination(for: Route.self) { route in ... }
}

5. Testovanie navigácie

S Router patternom je testovanie navigácie hračka — pretože navigačná logika je v samostatnej triede, nie v UI kóde:

@Test func testNavigaciaNaDetail() {
    let router = AppRouter()

    router.navigovatNa(.detailClanku(id: 42))

    #expect(router.cesta.count == 1)
}

@Test func testSpatNaRoot() {
    let router = AppRouter()
    router.navigovatNa(.zoznamClankov)
    router.navigovatNa(.detailClanku(id: 1))
    router.navigovatNa(.profil(uzivatelId: 5))

    router.spatNaRoot()

    #expect(router.cesta.isEmpty)
}

@Test func testDeepLink() {
    let router = AppRouter()
    let url = URL(string: "mojaapp://clanok/123")!

    router.spracujDeepLink(url)

    // Overenie, že router správne spracoval deep link
    #expect(router.cesta.count == 1)
}

Záver

Navigácia v SwiftUI prešla za posledné roky obrovským vývojom. Od jednoduchého (ale dosť limitovaného) NavigationView sme sa dostali k plne programatickej, typovo bezpečnej navigácii s NavigationStack a NavigationPath. S Router a Coordinator patternom máme k dispozícii architektonické vzory, ktoré škálujú aj pre tie najkomplexnejšie aplikácie.

Kľúčové body na zapamätanie:

  • NavigationStack nahradil NavigationView — používajte value-based navigáciu s .navigationDestination(for:)
  • NavigationPath je váš najlepší priateľ pre navigáciu s rôznymi typmi a serializáciu stavu
  • Programatická navigácia je jednoduchá — stačí manipulovať s poľom alebo cestou
  • Deep linking centralizujte do jedného parsera a routera
  • Router pattern je ideálny pre väčšinu produkčných aplikácií
  • Coordinator pattern použite pri veľkých, modulárnych aplikáciách
  • V iOS 26 navigácia automaticky získava Liquid Glass dizajn bez zmien kódu

Ak ste dočítali až sem, máte všetko potrebné na to, aby ste vo svojej SwiftUI aplikácii implementovali robustnú a udržateľnú navigáciu. A ak máte akékoľvek otázky alebo vlastné tipy, neváhajte sa podeliť v komentároch.

O Autorovi Editorial Team

Our team of expert writers and editors.