SwiftUI NavigationStack 2026: opas ohjelmalliseen navigointiin ja koordinaattorimalliin
Käytännön opas SwiftUI:n NavigationStackiin: arvopohjainen NavigationLink, NavigationPath, tyyppiturvalliset reitit enum-arvoilla, koordinaattorimalli, syvälinkit ja iOS 26:n tunnetut bugit sekä niiden kiertotiet.
SwiftUI:n NavigationStack on iOS 16:ssa esitelty datavetoinen navigointirakenne, joka korvaa vanhentuneen NavigationView:n ja antaa täyden ohjelmallisen kontrollin näkymäpinoon NavigationPath-tyypin avulla. Vuonna 2026, kolmen vuoden iteraation jälkeen, NavigationStack on vihdoin se ratkaisu, jota olen odottanut: tyyppiturvallinen reititys, syvälinkkien luonnollinen tuki ja koordinaattorimallin toteutus ilman kolmannen osapuolen kirjastoja. Törmäsin ihan viime kuussa tuotannossa kolmeen iOS 26 -bugiin, jotka ehkä sinäkin kohtaat, joten kokosin tähän oppaaseen kaiken sen, mitä tarvitset toimivan navigointiarkkitehtuurin rakentamiseen iOS 26 -kohteessa. Saavutettavuus mukana alusta lähtien (ei jälkiajatuksena, kuten liian usein näkee).
NavigationStack korvaa NavigationView:n iOS 16:sta alkaen ja käsittelee navigointia datana NavigationPath-tyypin kautta.
Käytä arvopohjaista NavigationLink(value:)-alustinta yhdessä .navigationDestination(for:)-modifierin kanssa. Vanha lapsinäkymän hyväksyvä NavigationLink-tyyli on epäkäytännöllinen kaikessa muussa kuin triviaaleissa demoissa.
Määrittele reitit enum-tyypillä, joka toteuttaa Hashable-protokollan. Saat kääntäjän tuen ja mahdollistat koordinaattorin injektoinnin ympäristöön.
Palaa juureen asettamalla path = NavigationPath() tai path.removeLast(path.count). Molemmat toimivat, mutta jälkimmäinen välttää iOS 18:n .searchable-bugin.
iOS 26 sisältää kolme tunnettua NavigationStack-regressiota: TabView-navigointi rikkoutuu 26.1:ssä, .navigationTransition(.zoom)-eleen takaisinpyyhkäisy on epävakaa, ja DocumentGroup+NavigationSplitView-yhdistelmä ei toimi.
Saavutettavuus vaatii .accessibilityLabel-arvot navigointielementeille ja .accessibilityAddTraits(.isHeader)-määrityksen kohdenäkymän otsikolle, jotta VoiceOver lukee reitin oikein.
Mikä NavigationStack on ja miksi NavigationView on vanhentunut?
NavigationStack on SwiftUI-säiliö, joka esittää juurinäkymän ja tarjoaa mahdollisuuden esittää lisää näkymiä pinoon joko lapsinäkymän linkkien tai NavigationPath-sidonnan kautta. Se korvasi NavigationView:n iOS 16:ssa. Tärkein arkkitehtoninen muutos on, että navigointi mallinnetaan datana, ei näkymähierarkiana. Sinulla on taulukko (tai NavigationPath) arvoja, ja SwiftUI päättelee, mitä näyttää, kutsumalla .navigationDestination(for:)-modifieria kutakin arvoa kohden.
Käytännön ero on valtava. Vanha NavigationView vaati sinut esittämään kohdenäkymän NavigationLink-alustimen sulkeumassa, mikä tarkoitti sitä, että ohjelmallinen navigointi vaati @State-boolean-lippuja tai tag/selection-yhdistelmiä. Syvälinkitys? Vaati Rube Goldberg -kaltaisia rakennelmia. Tila-tila-siirtymät karkasivat käsistä. NavigationStack ratkaisee tämän kääntämällä navigoinnin taulukkomanipulaatioksi: path.append(reitti) työntää näkymän, path.removeLast() pop-aa sen. Ihan näin yksinkertaista.
Applen virallinen NavigationStack-dokumentaatio kuvaa API:n muodollisesti, mutta muistisääntönä: jos kirjoitat uutta koodia iOS 16 -kohteeseen tai uudempaan, käytä NavigationStack-tyyppiä. NavigationView ei ainoastaan ole vanhentunut, vaan se tuottaa vuonna 2026 kääntäjävaroituksia ja saattaa käyttäytyä eri tavoin eri iOS-versioissa.
Arvopohjainen NavigationLink ja navigationDestination
NavigationStackin voimakkain kaava on arvopohjainen NavigationLink yhdistettynä .navigationDestination(for:)-modifieriin. Sen sijaan, että työnnät kohdenäkymän linkin sisään, työnnät arvon (tavallisesti tunnisteen tai enum-tapauksen), ja SwiftUI etsii vastaavan navigationDestination-modifierin pinon näkymäpuusta.
struct ArtikkeliLista: View {
let artikkelit: [Artikkeli]
var body: some View {
NavigationStack {
List(artikkelit) { artikkeli in
// Työntää arvon, ei näkymää. Koodi pysyy ohuena.
NavigationLink(artikkeli.otsikko, value: artikkeli.id)
}
.navigationTitle("Artikkelit")
// Modifier täytyy sijoittaa NavigationStackin SISÄLLE
.navigationDestination(for: Artikkeli.ID.self) { id in
ArtikkeliNakyma(artikkeliId: id)
}
}
}
}
Kolme tärkeää sääntöä, jotka rikkovat noin 90 % ongelmista, joita näen kollegoiden koodissa:
Sijoita .navigationDestination NavigationStackin sisään, ei sen ulkopuolelle. Muuten saat suoritusajan varoituksen: "A NavigationLink is presenting a value ... but there is no matching navigation destination visible from the location of the link."
Älä liitä .navigationDestination-modifieria itse NavigationStack-säiliöön. Liitä se juurinäkymään pinon sisällä (tässä esimerkissä List-näkymään). SwiftUI:n sisäinen etsintä ei löydä säiliöstä liitettyä modifieria luotettavasti.
Yksi .navigationDestination-kutsu per arvotyyppi per NavigationStack. Jos rekisteröit kaksi .navigationDestination(for: Int.self)-kutsua saman pinon sisällä, viimeisin voittaa hiljaa, ja jos se on eri näkymässä, käyttäytyminen muuttuu odottamattomasti.
Arvopohjainen malli avaa myös tyyppiturvallisuuden. Kääntäjä tarkistaa, että työntämäsi arvo vastaa navigationDestinationin odottamaa tyyppiä. Refaktoroinnit kulkevat kääntäjän kautta sen sijaan, että ne murtuisivat suoritusaikana. Rehellisesti sanoen tämä yksin oli syy, miksi lakkasin kokonaan käyttämästä NavigationView-tyyppiä uusissa projekteissa.
NavigationPath ja ohjelmallinen navigointi
NavigationPath on tyyppipoistoinen kokoelma, joka voi sisältää mitä tahansa Hashable-arvoja. Se on ratkaisu, kun pinossa tarvitaan sekatyyppejä (esim. Artikkeli.ID ja Kayttaja.ID), tai kun aiot palauttaa navigointitilan verkkopalvelusta. Sidot sen NavigationStack-alustimeen ja saat täyden ohjelmallisen kontrollin.
struct SovelluksenJuuri: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
EtusivuNakyma()
.navigationDestination(for: Artikkeli.ID.self) { id in
ArtikkeliNakyma(artikkeliId: id) {
// Työnnä toinen näkymä artikkeli-näkymästä
path.append(Kommentit.ID(artikkeliId: id))
}
}
.navigationDestination(for: Kommentit.ID.self) { kommenttiId in
KommenttiNakyma(id: kommenttiId)
}
}
}
// Ohjelmallinen navigointi ulkopuolelta (esim. push-ilmoitus)
func avaaArtikkeli(_ id: Artikkeli.ID) {
path.append(id)
}
}
Jos tiedät kaikki reitit etukäteen ja ne käyttävät yhtä tyyppiä, tavallinen taulukko (@State private var path: [Reitti] = []) on kevyempi ja debuggautuvampi kuin tyyppipoistoinen NavigationPath. Käytän sitä oletuksena ja siirryn NavigationPath-tyyppiin vasta silloin, kun tarve monityyppiselle pinolle on todellinen. En koskaan spekulatiivisesti.
Tyyppiturvalliset reitit enum-arvoilla
Yksittäisen tunnisteen työntäminen toimii pieniin sovelluksiin, mutta oikeat sovellukset ansaitsevat enum-pohjaisen reittikaavion, joka toimii kääntäjän kanssa. Kaava on suoraviivainen:
enum Reitti: Hashable {
case artikkeli(id: Artikkeli.ID)
case profiili(kayttajaId: Kayttaja.ID)
case asetukset
case tilaus(vaihe: TilausVaihe)
}
enum TilausVaihe: Hashable {
case ostoskori
case toimitus
case maksu
case vahvistus
}
struct SovelluksenJuuri: View {
@State private var path: [Reitti] = []
var body: some View {
NavigationStack(path: $path) {
EtusivuNakyma()
.navigationDestination(for: Reitti.self) { reitti in
switch reitti {
case .artikkeli(let id):
ArtikkeliNakyma(id: id)
case .profiili(let kayttajaId):
ProfiiliNakyma(id: kayttajaId)
case .asetukset:
AsetuksetNakyma()
case .tilaus(let vaihe):
TilausVaiheNakyma(vaihe: vaihe)
}
}
}
}
}
switch-lauseen tyhjentävyys on koko pointti: kun lisäät uuden reitin, kääntäjä epäonnistuu, kunnes käsittelet sen. Ei enää unohdettuja tapauksia. Kun yhdistät tämän SwiftUI:n @Observable-makroon, saat myös reaktiivisen tilanhallinnan navigoinnin ympärille ilman ObservableObject-vanhentuneita malleja.
Koordinaattorimalli SwiftUI:ssa
Kun sovelluksesi kasvaa, navigointilogiikan sirottelu yksittäisiin näkymiin muuttuu velaksi. Koordinaattorimalli (jonka UIKit-kehittäjät tuntevat vuosien takaa) sopii NavigationStackiin lähes maagisesti, koska navigointi on nyt dataa, jota koordinaattori voi hallita.
import SwiftUI
import Observation
@Observable
final class SovellusReititin {
var polku: [Reitti] = []
func naytaArtikkeli(_ id: Artikkeli.ID) {
polku.append(.artikkeli(id: id))
}
func aloitaTilaus() {
polku.append(.tilaus(vaihe: .ostoskori))
}
func etene(seuraavaan vaihe: TilausVaihe) {
polku.append(.tilaus(vaihe: vaihe))
}
func palautaJuureen() {
polku.removeAll()
}
}
// Injektoi juuressa
@main
struct SwiftCraftedSovellus: App {
@State private var reititin = SovellusReititin()
var body: some Scene {
WindowGroup {
NavigationStack(path: $reititin.polku) {
EtusivuNakyma()
.navigationDestination(for: Reitti.self) { reitti in
NakymaTehdas.tee(reitti)
}
}
.environment(reititin)
}
}
}
// Käytä syvällä lapsinäkymässä
struct ArtikkeliRivi: View {
let artikkeli: Artikkeli
@Environment(SovellusReititin.self) private var reititin
var body: some View {
Button(artikkeli.otsikko) {
reititin.naytaArtikkeli(artikkeli.id)
}
}
}
Tämä kaava eristää navigointilogiikan yhteen paikkaan, jota voit yksikkötestata ilman SwiftUI-runtime-instansseja. Testi voi vain kutsua reititin.naytaArtikkeli(id) ja väittää, että polku-taulukko sisältää oikean arvon. Yksinkertaista ja kaunista.
Syvälinkkien toteutus URL-käsittelyllä
Syvälinkkien käsittely on paikka, jossa NavigationStack todella loistaa. Käsittelet saapuvan URL:n .onOpenURL-modifierissa (tai onContinueUserActivity-modifierissa universaaleille linkeille), jäsennät sen reitiksi ja työnnät sen polkuun. SwiftUI hoitaa loput.
extension SovellusReititin {
func kasitteleUrl(_ url: URL) {
guard let osat = URLComponents(url: url, resolvingAgainstBaseURL: false),
let polkuOsat = osat.path.split(separator: "/").first
else { return }
switch polkuOsat {
case "artikkeli":
// esim. swiftcrafted://artikkeli/123
if let idString = url.pathComponents.last,
let id = UUID(uuidString: idString) {
polku = [.artikkeli(id: id)]
}
case "tilaus":
polku = [.tilaus(vaihe: .ostoskori)]
default:
break
}
}
}
// Sovellustasolla
.onOpenURL { url in
reititin.kasitteleUrl(url)
}
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
if let url = activity.webpageURL {
reititin.kasitteleUrl(url)
}
}
Huomaa, että asetan polku = [.artikkeli(id: id)] sen sijaan, että kutsuisin append-metodia. Syvälinkin pitäisi korvata pino, ei kasata sitä olemassa olevan päälle. Muuten käyttäjä saa outoja tilanteita, joissa takaisin-nappi vie edelliseen istuntoon (kysy vaikka minulta, kokeilin kerran push-ilmoituksen kautta, ja lopputulos oli hämmentävä). Applen universaalien linkkien dokumentaatio kattaa Associated Domains -konfiguroinnin, jota tarvitset tuotannossa.
Miten palautan pinon juureen?
Palauta pino juureen asettamalla polku tyhjäksi. Jos käytät taulukkoa, kirjoita polku = []. Jos käytät NavigationPath-tyyppiä, kirjoita polku = NavigationPath() tai polku.removeLast(polku.count). Molemmat ovat kelvollisia, mutta ne kaksi eroavat yhdessä käyttötapauksessa, joka söi minulta monta iltaa: iOS 18:n .searchable-bugi.
// Toimii yleensä
reititin.polku = []
// Toimii myös, ja välttää iOS 18 .searchable-bugin
reititin.polku.removeLast(reititin.polku.count)
// TabView-tapauksessa: napauta aktiivista välilehteä uudelleen palauttaaksesi
struct RootTabView: View {
@State private var valilehti: Valilehti = .etusivu
@State private var etusivuPolku: [Reitti] = []
var body: some View {
TabView(selection: Binding(
get: { valilehti },
set: { uusi in
if uusi == valilehti && uusi == .etusivu {
etusivuPolku = [] // Kaksoisnapautus palauttaa juureen
}
valilehti = uusi
}
)) {
NavigationStack(path: $etusivuPolku) { EtusivuNakyma() }
.tabItem { Label("Etusivu", systemImage: "house") }
.tag(Valilehti.etusivu)
// ...
}
}
}
Applen kehittäjäfoorumeissa on raportoitu bugi, jossa polku = []-asetus .searchable-tilassa ei aiheuta pop-animaatiota. Kiertotienä käytä removeLast-metodia, tai sulje ensin aktiivinen hakukenttä dismissSearch-EnvironmentValues-arvolla.
NavigationStack vs NavigationSplitView
Molemmat ovat modernin SwiftUI-navigoinnin osia, mutta ne ratkaisevat eri ongelmia. NavigationStack on push/pop-pino iPhonen tyylisiin lineaarisiin virtoihin. NavigationSplitView puolestaan on kaksi- tai kolmisarakkeinen näkymä iPadin ja Macin master-detail-käyttöliittymiin, joka lyhentyy automaattisesti pinoksi iPhonessa.
Ominaisuus
NavigationStack
NavigationSplitView
Pääkäyttötapaus
Lineaarinen push/pop-virta
Master-detail (iPad, Mac, iPhone maisema)
Sarakkeiden määrä
Yksi
Kaksi tai kolme
Ohjelmallinen API
NavigationPath tai taulukko
Valinta-Bindings per sarake
Automaattinen mukautuminen iPhone-yhteen
Ei tarvitse
Kyllä, lyhentyy pinoksi
Sopii TabView-lapseksi
Kyllä (yksi per välilehti)
Kyllä, mutta harvoin oikea valinta
Syvälinkkien tuki
Erinomainen (polkuarvo)
Hyvä (valintasidonnat)
iPadOS-Sidebar-tuki
Ei
Alkuperäinen tuki
Käytännön säännöni: jos näyttö on iPhone-only tai virta on selkeästi lineaarinen (kirjautuminen, tilaus, sovittelija), käytä NavigationStack-tyyppiä. Jos rakennat sisällönhallinnan (viestit, muistiinpanot, dokumenttiselain), käytä NavigationSplitView-tyyppiä. Voit sijoittaa NavigationStack-tyypin NavigationSplitView-tyypin detail-sarakkeen sisään. Mutta ÄLÄ sivupalkin sarakkeen sisään, kuten Apple Developer -foorumeissa on todettu.
iOS 26 -bugit ja kiertotiet
iOS 26 sisältää useita NavigationStack-regressioita, jotka minun on täytynyt kiertää tuotantosovelluksissa. Nämä eivät ole spekulaatiota, vaan ne on dokumentoitu Applen kehittäjäfoorumeissa:
TabView + NavigationStack rikkoutuu iOS 26.1:ssä. Ei-aktiivisen välilehden NavigationStack-polkuun työnnetyt arvot ohitetaan hiljaa. Kiertotie: viivytä path.append-kutsua, kunnes välilehden aktivointi on valmis (Task { try? await Task.sleep(for: .milliseconds(50)); path.append(...) }).
Zoom-navigaatiosiirtymän takaisinpyyhkäisy on epävakaa. Kun käytät .navigationTransition(.zoom(sourceID: id, in: namespace))-modifieria, reunapyyhkäisyele takaisin joko epäonnistuu tai näyttää eleen visuaalisia artefakteja. Kiertotie: älä käytä zoom-siirtymää, kunnes se on korjattu, pysy oletusliukusiirtymässä, joka toimii moitteettomasti.
DocumentGroup + NavigationSplitView ei toimi. Takaisin-nappi ja dokumentin otsikko renderöityvät kahdesti iPhonella. Kiertotie: käytä pelkkää NavigationStack-tyyppiä DocumentGroup-tyypin sisällä, kunnes iOS 26.2 julkaistaan.
Navigointiotsikko rullaa sisällön alle. Kun sijoitat ScrollView-näkymän NavigationStack-tyypin sisään, otsikkopalkki menettää läpinäkymättömyytensä rullauksen aikana. Kiertotie: lisää .toolbarBackground(.visible, for: .navigationBar) juurinäkymään.
Rinnakkaisuuden ja päänäkymän elinkaaren yhteispeli on toinen paikka, jossa asiat menevät pieleen. Swift 6.2 -rinnakkaisuusoppaassani käsittelen, miten @MainActor-eristys vaikuttaa navigointireitittimen turvallisuuteen. Lyhyesti: merkitse reititin @MainActor-attribuutilla, tai altistat itsesi tietokilpailuille.
Saavutettavuus ja VoiceOver
Navigointi on paikka, jossa saavutettavuus joko toimii sujuvasti tai murtuu. Kun VoiceOver-käyttäjä työntää uuden näkymän NavigationLink-linkillä, SwiftUI ilmoittaa siirtymän ja siirtää fokuksen kohdenäkymän ensimmäiseen saavutettavaan elementtiin. Mutta vain, jos otsikko on merkitty otsikkotunnisteella.
struct ArtikkeliNakyma: View {
let artikkeli: Artikkeli
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text(artikkeli.otsikko)
.font(.largeTitle)
// VoiceOver lukee tämän otsikkona, ei tavallisena tekstinä
.accessibilityAddTraits(.isHeader)
Text(artikkeli.leipateksti)
}
.padding()
}
.navigationTitle(artikkeli.otsikko)
// Piilota navigointipalkin dupliaattiotsikko VoiceOverilta
.navigationBarTitleDisplayMode(.inline)
}
}
Kolme sääntöä, joita noudatan jokaisessa NavigationStack-näkymässä:
Merkitse jokainen otsikkoteksti .accessibilityAddTraits(.isHeader)-modifierilla, jotta VoiceOver voi liikkua otsikoiden välillä rotorilla.
Anna kuvakepohjaisille NavigationLink-elementeille .accessibilityLabel-arvo. SF Symbols -kuvake yksin ei kerro VoiceOver-käyttäjälle, mihin linkki johtaa.
Testaa Reduce Motion-tilaa. NavigationStackin oletussiirtymä kunnioittaa asetusta, mutta jos käytät .navigationTransition(.zoom)-modifieria, sinun on tarkistettava UIAccessibility.isReduceMotionEnabled-arvo ja poistettava se käytöstä.
Animaatioajoitus on myös saavutettavuuskysymys. Liian nopea siirtymä hämmentää joitain käyttäjiä, ja liian hidas turhauttaa muita. Applen oletusarvo (~0,35 s hidastuvalla käyrällä) on huolellisesti kalibroitu, älä muuta sitä ilman käytettävyystestausta. Jos rakennat iOS 26:n uutta Liquid Glass -suunnittelukieltä, materiaalikerrokset lisäävät oman visuaalisen painonsa, ja siirtymän on tunnuttava sen kanssa yhtenäiseltä.
Usein kysytyt kysymykset
Voinko käyttää NavigationStackiä iOS 15 -kohteessa?
En. NavigationStack vaatii iOS 16 -käyttöjärjestelmää tai uudempaa. Jos sinun on tuettava iOS 15:tä, käytä NavigationView-tyyppiä if #available-tarkistuksen kautta ja siirry NavigationStack-tyyppiin heti, kun minimikohde nousee.
Miksi navigationDestination-modifieri ei laukaise kohdetta?
Yleisin syy on, että olet sijoittanut modifierin NavigationStack-säiliön ulkopuolelle tai kiinnittänyt sen suoraan NavigationStack-tyyppiin (juurinäkymän sijaan). Siirrä modifier juurinäkymään pinon sisällä, jolloin varoitus katoaa ja navigointi toimii.
Kumpi on parempi: NavigationPath vai tyypitetty taulukko?
Tyypitetty taulukko (esim. [Reitti]) on parempi, kun kaikki reitit jakavat yhden tyypin. Saat kääntäjän tarkistukset ja debuggauksen. NavigationPath on tarpeen vain, kun pinon on sisällettävä sekatyyppejä. Aloita taulukolla, siirry vasta tarpeen mukaan.
Miten hallitsen syvälinkkejä koordinaattorimallissa?
Käsittele saapuva URL .onOpenURL-modifierissa juurinäkymässä, jäsennä se reitti-enum-arvoksi ja aseta koordinaattorin polku suoraan sen sijaan, että kutsuisit append-metodia. Näin syvälinkin pino ei kasaudu olemassa olevan istunnon päälle.
Toimiiko NavigationStack TabViewin sisällä iOS 26:ssa?
Toimii iOS 26.0:ssa, mutta iOS 26.1:ssä on regressio, jossa ei-aktiiviselle välilehdelle työnnetyt polkuarvot ohitetaan hiljaa. Kiertotienä viivytä append-kutsua noin 50 ms:llä välilehden aktivoinnin jälkeen. Apple on kuittanut bugin virallisessa Feedback-järjestelmässä.
Kuinka teen NavigationStack-navigoinnista saavutettavan VoiceOverille?
Merkitse kohdenäkymän pääotsikko .accessibilityAddTraits(.isHeader)-modifierilla, anna kuvakepohjaisille linkeille .accessibilityLabel-arvo ja käytä oletusnavigointisiirtymää. Se kunnioittaa Reduce Motion -asetusta automaattisesti. VoiceOver ilmoittaa navigoinnin siirtymän, jos otsikko on asetettu.
Näin profiloit SwiftUI-suorituskyvyn Xcode 26:n Instrumentsilla vuonna 2026: uusi SwiftUI-malli, Cause-and-Effect Graph, @Observable-makro ja ProMotion 120 Hz -budjetti käytännön koodiesimerkein.
SwiftUI:n .sensoryFeedback-modifier on iOS 26:ssa suositeltu tapa laukaista haptista palautetta. Käytännön opas kaikkiin SensoryFeedback-tyyppeihin, Core Hapticsiin ja saavutettavaan haptiikkaan koodiesimerkein ja UX-säännöin.
Opi rakentamaan App Intents Swiftissä ja altistamaan sovelluksesi toiminnot Siriin, Shortcutsiin ja Apple Intelligenceen. Koodiesimerkit, alustavertailu ja käytännön vinkit.