@Entry macro u SwiftUI: Vodič za prilagođene EnvironmentValues u iOS 26

Praktični vodič za @Entry macro u SwiftUI: zamjenjuje EnvironmentKey boilerplate, radi za PreferenceKey, FocusedValues i ContainerValues, uz primjere za iOS 26 i Swift 6.

@Entry Macro SwiftUI vodič iOS 26
On this page
  1. Što je @Entry macro i zašto postoji
  2. Osnovni primjer: prilagođena EnvironmentValue u jednom retku
  3. Kako migrirati sa starog EnvironmentKey pristupa
  4. @Entry za PreferenceKey i FocusedValues
  5. ContainerValues u iOS 26 i custom Group API
  6. Opcijski i generički tipovi s @Entry
  7. @MainActor izolacija i Swift 6 strict concurrency
  8. Pristupačnost, VoiceOver i testiranje
  9. Česte greške i kako ih izbjeći
  10. Često postavljana pitanja

Ažurirano: 11. rujna 2026.

@Entry macro je Swift makro koji jednim redom koda dodaje prilagođeni ključ u EnvironmentValues, PreferenceKey, FocusedValues ili ContainerValues, bez pisanja pomoćnih struktura, statičkih varijabli i extension blokova. Uveden je u iOS 18 (Xcode 16), a u iOS 26 s Xcode 26 dobio je bolju dijagnostiku, potporu za @MainActor izolaciju i integraciju s novim Group subview API-jem. U ovom vodiču pokazujem kako ga koristim u produkcijskom SwiftUI kodu, gdje se lomi i kako ga uvezati u pristupačnost bez naknadne pameti.

Što je @Entry macro i zašto postoji

@Entry je macro deklariran u SwiftUI-ju koji generira kompletan EnvironmentKey conformance (ili odgovarajući ključ za PreferenceKey, FocusedValues, ContainerValues) na temelju jedne stored property deklaracije. Prije njega, dodavanje jedne prilagođene environment vrijednosti tražilo je zasebnu strukturu koja implementira protokol EnvironmentKey, statički defaultValue, te extension nad EnvironmentValues s getter/setter parom. To je bilo mehanički, ali dovoljno zamorno da su timovi izbjegavali granulirane vrijednosti i gurali sve u jedan veliki @Observable objekt.

Iskreno, prvi put sam se prebacila na @Entry na produkcijskom projektu jer smo u code reviewu imali cijele PR-ove koji su bili samo boilerplate za jedan mali flag. Macro je predstavljen na WWDC24 kao dio poteza prema tome da SwiftUI kod izgleda deklarativno i na razini definicije podataka, ne samo na razini pogleda. Interno se raspakira u istu strukturu koju bismo napisali ručno, pa se generirani kod može pregledati preko Expand Macro opcije u Xcodeu (desni klik na @Entry u kodu). Za dublji uvod u sam mehanizam Swift makroa preporučujem najprije pročitati vodič za Swift Macros u Swiftu 6, koji objašnjava razliku između freestanding i attached macroa te kako MacroExpansion radi u praksi.

Osnovni primjer: prilagođena EnvironmentValue u jednom retku

Pretpostavimo da radim komponentu koja mora znati koliko je pauza između koraka animacije. Prije @Entry, to je izgledalo ovako:

// Stari pristup (iOS 17 i ranije)
private struct StepDelayKey: EnvironmentKey {
    static let defaultValue: Duration = .milliseconds(120)
}

extension EnvironmentValues {
    var stepDelay: Duration {
        get { self[StepDelayKey.self] }
        set { self[StepDelayKey.self] = newValue }
    }
}

S @Entry macroom, isti kod svede se na tri retka:

// iOS 18 / iOS 26
extension EnvironmentValues {
    @Entry var stepDelay: Duration = .milliseconds(120)
}

Zatim ga čitam s @Environment property wrapperom u bilo kojem pogledu:

struct StepIndicator: View {
    @Environment(\.stepDelay) private var delay
    let steps: [String]

    var body: some View {
        HStack(spacing: 8) {
            ForEach(Array(steps.enumerated()), id: \.offset) { index, label in
                Text(label)
                    .opacity(0.4)
                    .animation(
                        .easeInOut(duration: 0.2).delay(delay.seconds * Double(index)),
                        value: label
                    )
                    .accessibilityLabel("Korak \(index + 1) od \(steps.count): \(label)")
            }
        }
    }
}

Na razini roditeljskog pogleda mijenjam vrijednost pozivom .environment(\.stepDelay, .milliseconds(80)). Sve što je već znano o @Environment-u i dalje vrijedi: propagacija ide kroz stablo, override se radi po podstablu, a defaultValue se koristi ako nitko iznad nije postavio vrijednost.

Kako migrirati sa starog EnvironmentKey pristupa

Migracija je mehanička i preporučam raditi je datoteku po datoteku, uz commit nakon svake (u mom zadnjem projektu smo pretvorili oko 40 EnvironmentKey struktura u jedno popodne, bez ijednog regresijskog bug-a). Za svaku postojeću EnvironmentKey strukturu:

  1. Zabilježi tip vrijednosti i defaultValue.
  2. Obriši struct koja implementira EnvironmentKey.
  3. U extensionu nad EnvironmentValues, zamijeni getter/setter par s jednom @Entry property deklaracijom, koristeći isti tip i defaultValue.
  4. Ako je stara implementacija imala private pristup za key struct, nova @Entry property može biti internal ili public ovisno o tome tko je koristi izvan modula.

Primjer prije i poslije:

// PRIJE
private struct ThemeAccentKey: EnvironmentKey {
    static let defaultValue: Color = .accentColor
}
extension EnvironmentValues {
    var themeAccent: Color {
        get { self[ThemeAccentKey.self] }
        set { self[ThemeAccentKey.self] = newValue }
    }
}

// POSLIJE
extension EnvironmentValues {
    @Entry var themeAccent: Color = .accentColor
}

@Entry za PreferenceKey i FocusedValues

Isti macro radi za tri druga SwiftUI mehanizma. Za PreferenceKey (kojim pogledi šalju vrijednosti gore prema roditelju) deklaracija izgleda ovako:

extension PreferenceValues {
    @Entry var measuredHeight: CGFloat = 0
}

Napomena: PreferenceValues je novi kontejnerski tip uveden u iOS 18 kao paralela za EnvironmentValues. Prije njega, svaka PreferenceKey struktura zahtijevala je ručnu reduce funkciju. @Entry pretpostavlja "posljednji nadjačava" (last-write-wins) reduce, što je najčešći slučaj. Ako trebate reduce koji zbraja ili spaja liste, i dalje pišite ručnu implementaciju.

Za FocusedValues, koji koristim kad root pogled treba znati što je fokusirano u nekoj sub-hijerarhiji (najčešće za macOS menije ili iPadOS keyboard shortcuts):

extension FocusedValues {
    @Entry var selectedDocument: Document? = nil
}

struct DocumentEditor: View {
    let doc: Document
    var body: some View {
        TextEditor(text: .constant(doc.body))
            .focusedValue(\.selectedDocument, doc)
    }
}

struct RenameCommand: Commands {
    @FocusedValue(\.selectedDocument) private var doc

    var body: some Commands {
        CommandMenu("Dokument") {
            Button("Preimenuj…") { rename(doc) }
                .disabled(doc == nil)
                .keyboardShortcut("r", modifiers: [.command])
        }
    }

    private func rename(_ doc: Document?) { /* … */ }
}

ContainerValues u iOS 26 i custom Group API

iOS 18 je uveo ContainerValues, mehanizam koji custom kontejnerskim pogledima omogućuje čitanje metapodataka o pojedinačnim child pogledima (recimo, "je li ovaj element istaknut" ili "koja mu je težina u layoutu"). U iOS 26 kombinacija @Entry + Group(subviews:) postala je preferirani način za pisanje custom containera koji se ponašaju kao List ili Menu, sa slot-baziranom API-jom, ali bez runtime introspekcije.

extension ContainerValues {
    @Entry var chipEmphasis: ChipEmphasis = .standard
}

enum ChipEmphasis { case standard, high }

extension View {
    func chipEmphasis(_ value: ChipEmphasis) -> some View {
        containerValue(\.chipEmphasis, value)
    }
}

struct ChipRow<Content: View>: View {
    @ViewBuilder var content: Content

    var body: some View {
        HStack(spacing: 8) {
            Group(subviews: content) { subviews in
                ForEach(subviews) { sub in
                    sub
                        .padding(.horizontal, 12)
                        .padding(.vertical, 6)
                        .background(
                            Capsule().fill(
                                sub.containerValues.chipEmphasis == .high
                                    ? .accentColor.opacity(0.2)
                                    : .secondary.opacity(0.1)
                            )
                        )
                }
            }
        }
    }
}

Poziv izgleda kao standardni SwiftUI: ChipRow { Text("Novo").chipEmphasis(.high); Text("Arhiva") }. Ovo je paralelno modernoj navigaciji koju sam opisala u vodiču za SwiftUI NavigationStack u iOS 26. SwiftUI ide u smjeru u kojem custom kontejneri prestaju biti "hack s _VariadicView" i postaju first-class API.

Opcijski i generički tipovi s @Entry

Macro podržava opcijske tipove, ali zahtijeva eksplicitni defaultValue, obično nil. Također radi s generičkim tipovima ako je konkretan tip poznat na mjestu deklaracije property-ja.

extension EnvironmentValues {
    // Opcijski
    @Entry var currentUser: User? = nil

    // Generički s konkretnom instancom u defaultu
    @Entry var itemFormatter: any FormatStyle<Item, String> = DefaultItemFormatter()

    // Struktura s memberwise defaultom
    @Entry var layoutMetrics: LayoutMetrics = LayoutMetrics(spacing: 8, corner: 12)
}

Ako defaultValue nije jeftin za izračunati (npr. otvara datoteku ili radi mrežni poziv), zamotajte ga u lazy pomoćnu strukturu ili koristite () -> T closure kao tip. Sam macro ne radi lazy inicijalizaciju: defaultValue se evaluira svaki put kad ga netko iznad nije postavio. Točno na tome sam se opekla dok sam ubacila Bundle-load u default vrijednost i onda tražila zašto se app pri hladnom startu vuče.

@MainActor izolacija i Swift 6 strict concurrency

Pod Swift 6 strict concurrency modelom, SwiftUI pogledi su @MainActor-izolirani. To znači da vrijednosti koje čuvate u EnvironmentValues moraju biti ili Sendable ili same @MainActor-izolirane. @Entry macro će u iOS 26 SDK-u ispisati jasnu dijagnostiku ako izberete tip koji ne zadovoljava ovaj uvjet:

// ⚠️ Upozorenje u Swift 6 strict mode:
// "Type 'MutableCache' does not conform to 'Sendable'"
extension EnvironmentValues {
    @Entry var cache: MutableCache = MutableCache()
}

Rješenje je učiniti tip Sendable, koristiti @Observable klasu s odgovarajućom izolacijom, ili prebaciti mutacije u actor. Za detaljniji uvod u strict concurrency preporučujem svoj vodič za Swift Concurrency, koji objašnjava kako Sendable checker radi i kako migrirati postojeći kod bez lomljenja.

Pristupačnost, VoiceOver i testiranje

Prilagođene environment vrijednosti često nose informacije koje utječu na pristupačnost, recimo "koristi li korisnik smanjeno kretanje" ili "koja je preferirana dinamička veličina teksta". @Entry ne mijenja ništa oko toga, ali daje priliku da te preference eksplicitno modelirate umjesto da ih razbacujete po pogledima.

extension EnvironmentValues {
    @Entry var reducedAnimation: ReducedAnimationPolicy = .system
}

enum ReducedAnimationPolicy: Sendable {
    case system   // slijedi UIAccessibility.isReduceMotionEnabled
    case always
    case never
}

struct FadeIn: ViewModifier {
    @Environment(\.reducedAnimation) private var policy
    @Environment(\.accessibilityReduceMotion) private var systemReduce

    func body(content: Content) -> some View {
        let shouldAnimate: Bool = {
            switch policy {
            case .system: return !systemReduce
            case .always: return true
            case .never: return false
            }
        }()
        return content
            .opacity(1)
            .animation(shouldAnimate ? .easeInOut(duration: 0.25) : nil, value: shouldAnimate)
    }
}

VoiceOver ponašanje ostaje ono što ste postavili preko .accessibilityLabel, .accessibilityHint i .accessibilityValue. Environment vrijednost samo modulira vizualnu animaciju. Testiranje se svede na standardni SwiftUI pristup: instancirajte pogled u testu, postavite environment vrijednost, i snimite snapshot ili provjerite ViewInspector-om. Za asinkrone testove koji se oslanjaju na custom environment, koristite #expect iz Swift Testing frameworka u Xcode 16, koji ima ugodniju sintaksu od XCTAssert za ovakve deklarativne provjere.

Česte greške i kako ih izbjeći

1. Zaboravljen defaultValue

@Entry var foo: Int bez defaulta neće se kompajlirati. Macro treba znati što vratiti kad nitko iznad nije postavio vrijednost. Ako defaultValue nema smisla (npr. "korisnik mora biti prijavljen"), koristite opcijski tip s nil defaultom i early-return u pogledu.

2. Miješanje starog i novog pristupa u istom fileu

Nemojte držati EnvironmentKey struct i @Entry property za istu vrijednost paralelno. Dobit ćete dvije različite defaultValue instance i tiho neusklađeno ponašanje. Migrirajte u cijelosti.

3. Krivi kontejner

@Entry u extensionu nad EnvironmentValues daje vrijednost koja teče prema dolje. U PreferenceValues daje vrijednost koja putuje prema gore. U ContainerValues daje vrijednost koju custom kontejner čita po child pogledu. Krivi izbor je najčešći uzrok "moj environment ne dolazi do child pogleda" bugova (upravo taj bug sam lovila skoro sat vremena prije nego sam skužila da sam property stavila u ContainerValues umjesto u EnvironmentValues).

4. Ne-Sendable tip pod Swift 6

Ako radite s @Observable klasom, čuvajte je izvan environmenta i injektirajte je preko .environment(myModel) (moderni Observable API), a ne kao @Entry var myModel: MyModel. Environment ključevi trebaju biti za jednostavne, konfiguracijske vrijednosti, ne za stateful modele.

Često postavljana pitanja

Koja je razlika između @Entry i @Environment?

@Entry se koristi u extensionu nad EnvironmentValues (ili PreferenceValues, FocusedValues, ContainerValues) i deklarira novu vrijednost. @Environment je property wrapper koji se koristi u pogledima i čita tu vrijednost preko keypath-a. Deklarirate jednom s @Entry, čitate više puta s @Environment.

Radi li @Entry macro u iOS 17 ili niže?

Ne. @Entry zahtijeva iOS 18 SDK (Xcode 16) na compile-time, i deploy target iOS 17 ili viši u praksi jer generirani kod koristi API-je uvedene u iOS 18. Za projekte koji podržavaju iOS 16/17, i dalje koristite stari EnvironmentKey pristup.

Mogu li @Entry macro koristiti s @Observable klasama?

Tehnički da, ali se ne preporučuje. Za @Observable modele koristite .environment(myModel) modifier i @Environment(MyModel.self) u child pogledima (to je posebni put koji SwiftUI koristi za observable objekte). @Entry je namijenjen konfiguracijskim vrijednostima (boje, razmaci, formatteri, flagovi), ne stateful modelima.

Kako debagirati kad prilagođena EnvironmentValue ne dolazi do child pogleda?

Prva provjera: koristite li .environment(\.key, value) na roditelju koji je zaista predak child pogleda? Sheet-ovi i full screen coveri imaju odvojeno environment stablo u nekim slučajevima. Druga provjera: nije li neki međupogled slučajno postavio istu vrijednost natrag na default. Xcode View Debugger u iOS 26 pokazuje environment override po pogledu, pa ga koristite umjesto print-debugginga.

Postoji li performance overhead u odnosu na ručni EnvironmentKey?

Ne. Macro se raspakira u točno isti kod koji biste napisali ručno, i možete ga vidjeti preko Xcode Expand Macro opcije. Nema runtime dispatch-a, refleksije ili alokacija dodatnih objekata. Performance profil je identičan starom pristupu.