Ú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.