SwiftUI @Observable -makro: Kattava opas Observation-kehykseen

Kattava suomenkielinen opas SwiftUI:n @Observable-makroon. Opi siirtymään ObservableObjectista, vältä yleisimmät sudenkuopat ja hyödynnä ominaisuustason seuranta. Sisältää iOS 26:n UIKit-tuen ja SE-0506:n uutuudet.

Johdanto: Tilanhallinta uudistui perusteellisesti

Jos olet kehittänyt SwiftUI-sovelluksia vuodesta 2019 lähtien, muistat varmasti ObservableObject-protokollan ja @Published-property wrapperin. Ne toimivat — mutta rehellisesti sanottuna, eivät aina kovin tehokkaasti. Joka kerta kun mikä tahansa @Published-ominaisuus muuttui, kaikki kyseistä objektia tarkkailevat näkymät piirrettiin uudelleen. Monimutkaisissa sovelluksissa tämä aiheutti ihan turhia suorituskykyongelmia.

WWDC 2023:ssa Apple esitteli Observation-kehyksen ja @Observable-makron, ja se muutti tilanhallinnan pelisääntöjä täysin. Swift 5.9:stä alkaen käytettävissä oleva @Observable tarkkailee muutoksia ominaisuustasolla — ei koko objektin tasolla. Käytännössä tämä tarkoittaa, että vain ne näkymät päivittyvät, jotka oikeasti lukevat muuttunutta ominaisuutta.

Vuonna 2026 tilanne on selvä: @Observable on Applen suosittelema tapa hallita tilaa uusissa SwiftUI-projekteissa. iOS 26 tuo lisäksi @Observable-tuen suoraan UIKitiin, ja hyväksytty SE-0506-ehdotus laajentaa kehystä edistyneillä tarkkailuominaisuuksilla.

Eli käydään tässä oppaassa läpi kaikki oleellinen: perusteista migraatioon, sudenkuopista suorituskyvyn optimointiin ja tuoreimpiin päivityksiin saakka.

Mikä on @Observable-makro?

@Observable on Swift-makro, joka tekee luokasta automaattisesti tarkkailtavan. Se korvaa vanhan ObservableObject-protokollan ja @Published-annotaatiot yhdellä siistillä makrolla. Kulissien takana makro generoi käännösaikana koodin, joka noudattaa Observable-protokollaa ja lisää jokaiselle tallennetulle ominaisuudelle automaattisen seurannan.

Ennen: ObservableObject

import Combine

class KayttajaProfiili: ObservableObject {
    @Published var nimi: String = ""
    @Published var sahkoposti: String = ""
    @Published var kuvanURL: URL? = nil
    @Published var onKirjautunut: Bool = false
}

Jälkeen: @Observable

import Observation

@Observable
class KayttajaProfiili {
    var nimi: String = ""
    var sahkoposti: String = ""
    var kuvanURL: URL? = nil
    var onKirjautunut: Bool = false
}

Huomaa kuinka paljon siistimpää tuo koodi on. Ei @Published-annotaatioita, ei Combine-riippuvuutta, ei protokollan noudattamista. Pelkkä @Observable luokan edessä, ja kaikki tallennetut ominaisuudet seurataan automaattisesti. Itse tykkään erityisesti siitä, miten paljon vähemmän "boilerplatea" uusi tapa vaatii.

Miten se toimii kulissien takana?

@Observable-makro generoi käännösaikana useita asioita:

  • Observable-protokollan mukaisuuden
  • ObservationRegistrar-instanssin, joka hallinnoi tarkkailua
  • Jokaiselle tallennetulle ominaisuudelle access- ja withMutation-kutsut gettereihin ja settereihin

Kun näkymä lukee ominaisuuden, access-kutsu rekisteröi säiekohtaiseen tallennustilaan, että tämä ominaisuus luettiin. Sitten kun ominaisuutta muutetaan, withMutation ilmoittaa vain niille näkymille, jotka todella lukivat kyseistä ominaisuutta. Aika nerokas systeemi.

Keskeiset erot ObservableObjectiin verrattuna

Ennen kuin hypätään käytännön esimerkkeihin, käydään nopeasti läpi tärkeimmät erot:

  • Saatavuus: ObservableObject iOS 13+ — @Observable iOS 17+
  • Kehys: ObservableObject käyttää Combinea — @Observable käyttää Observation-kehystä
  • Ominaisuusmerkintä: ObservableObject vaatii @Published@Observable ei vaadi mitään
  • Omistajuus näkymässä: @StateObject@State
  • Välittäminen: @ObservedObject → tavallinen let/var
  • Ympäristö: @EnvironmentObject@Environment
  • Sidokset: $viewModel.ominaisuus@Bindable
  • Päivitystarkkuus: Koko objekti → Yksittäinen ominaisuus
  • Lasketut ominaisuudet: Ei seurata automaattisesti → Seurataan automaattisesti

Property wrapperit: @State, @Bindable ja @Environment

Yksi suurimmista muutoksista on se, miten @Observable-objekteja käytetään näkymissä. Vanhat property wrapperit yksinkertaistuvat huomattavasti — ja tämä on mielestäni yksi parhaista puolista koko uudistuksessa.

@State — omistajuus ja elinkaari

Kun näkymä luo @Observable-instanssin, käytä @State. Se säilyttää instanssin näkymän uudelleenpiirrosten yli:

struct ProfiiliNakyma: View {
    @State private var viewModel = ProfiiliViewModel()
    
    var body: some View {
        VStack {
            Text(viewModel.nimi)
            Text(viewModel.sahkoposti)
        }
    }
}

Tärkeä sääntö: vain se näkymä, joka luo instanssin, käyttää @State. Kaikki muut näkymät vastaanottavat sen ihan tavallisena parametrina.

Tavallinen parametri — ei property wrapperia

Tämä on yksi niistä kohdista, joka tuntuu aluksi liian helpolta ollakseen totta. Kun näkymä vastaanottaa @Observable-objektin toiselta näkymältä, et tarvitse mitään property wrapperia. SwiftUI seuraa automaattisesti, mitä ominaisuuksia näkymä lukee:

struct NimiKomponentti: View {
    var viewModel: ProfiiliViewModel  // Ei wrapperia!
    
    var body: some View {
        Text(viewModel.nimi)
        // Päivittyy VAIN kun .nimi muuttuu
    }
}

@Bindable — kaksisuuntaiset sidokset

Kun tarvitset Binding-arvoja (esimerkiksi TextField-komponentissa), käytä @Bindable:

struct MuokkausNakyma: View {
    @Bindable var viewModel: ProfiiliViewModel
    
    var body: some View {
        Form {
            TextField("Nimi", text: $viewModel.nimi)
            TextField("Sähköposti", text: $viewModel.sahkoposti)
            Toggle("Kirjautunut", isOn: $viewModel.onKirjautunut)
        }
    }
}

@Bindable mahdollistaa $-syntaksin käytön suoraan @Observable-ominaisuuksien kanssa. Tämä korvaa vanhan mallin, jossa tarvittiin @ObservedObject ja @Published yhdessä sidosten luomiseksi.

@Environment — jaettu data näkymähierarkiassa

@Observable-objektit voidaan jakaa environment()-modifierin kautta, mikä korvaa vanhan environmentObject()-mallin:

// Juuri-näkymässä
@main
struct SovellukseniApp: App {
    @State private var kayttajaProfiili = KayttajaProfiili()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(kayttajaProfiili)
        }
    }
}

// Lapsi-näkymässä
struct AsetuksetNakyma: View {
    @Environment(KayttajaProfiili.self) private var profiili
    
    var body: some View {
        Text("Tervetuloa, \(profiili.nimi)!")
    }
}

Huomaa tyyppiturvallisuus: @Environment(KayttajaProfiili.self) käyttää konkreettista tyyppiä merkkijonoavaimen sijaan. Tämä on iso parannus virheenetsinnän kannalta.

Suorituskyvyn vallankumous: Ominaisuustason seuranta

Nyt päästään ihan asian ytimeen. @Observable-makron suurin etu on ominaisuustason seurannan tuoma suorituskyvyn parannus, ja ero vanhaan malliin on käytännössä merkittävä.

Ongelma vanhalla mallilla

// ObservableObject-malli
class TuoteListaViewModel: ObservableObject {
    @Published var tuotteet: [Tuote] = []
    @Published var hakutermi: String = ""
    @Published var jarjestys: Jarjestys = .nimi
    @Published var onLataamassa: Bool = false
}

struct HakuPalkki: View {
    @ObservedObject var viewModel: TuoteListaViewModel
    
    var body: some View {
        // Piirretään uudelleen kun MIKÄ TAHANSA
        // @Published-ominaisuus muuttuu!
        TextField("Hae...", text: $viewModel.hakutermi)
    }
}

Vanhassa mallissa HakuPalkki piirretään uudelleen aina kun tuotteet, jarjestys tai onLataamassa muuttuu — vaikka se lukee vain hakutermi-ominaisuutta. Kymmenientuhansien tuotteiden listassa tämä aiheuttaa ihan oikeaa ja tuntuvaa suorituskyvyn heikkenemistä.

Ratkaisu @Observable-mallilla

// @Observable-malli
@Observable
class TuoteListaViewModel {
    var tuotteet: [Tuote] = []
    var hakutermi: String = ""
    var jarjestys: Jarjestys = .nimi
    var onLataamassa: Bool = false
}

struct HakuPalkki: View {
    var viewModel: TuoteListaViewModel
    
    var body: some View {
        // Piirretään uudelleen VAIN kun
        // hakutermi muuttuu!
        TextField("Hae...", text: Bindable(viewModel).hakutermi)
    }
}

Nyt HakuPalkki päivittyy ainoastaan kun hakutermi muuttuu. Muiden ominaisuuksien muutokset eivät vaikuta tähän näkymään millään tavalla.

Kustannusmalli vertailussa

Ero tiivistyy näin:

  • Vanha malli: Päivityskustannus = objektia tarkkailevien näkymien määrä × minkä tahansa ominaisuuden muutostiheys
  • Uusi malli: Päivityskustannus = tiettyä ominaisuutta lukevien näkymien määrä × kyseisen ominaisuuden muutostiheys

Käytännössä tämä tarkoittaa, että kymmenientuhansien rivien lista voi päivittää yksittäisen rivin häiritsemättä muita. Kun rivi #42 muuttuu, vain se näkymä, joka luki item[42]:n, piirretään uudelleen. Aika huikea ero, eikö?

@ObservationIgnored: Ominaisuuksien poissulkeminen seurannasta

Joskus on ominaisuuksia, joiden muutoksista et yksinkertaisesti halua ilmoittaa näkymille. Tähän käytetään @ObservationIgnored-makroa:

@Observable
class AnalytiikkaViewModel {
    var naytettavaData: [DataPiste] = []
    var otsikko: String = "Kuukauden tilastot"
    
    @ObservationIgnored
    var valimuisti: [String: Any] = [:]
    
    @ObservationIgnored
    private var cancellables = Set<AnyCancellable>()
    
    @ObservationIgnored
    var viimeisinPaivitysaika: Date = .now
}

Tyypillisiä käyttökohteita @ObservationIgnored-makrolle:

  • Välimuistit ja sisäiset tallennusrakenteet
  • Combine-tilaajien cancellables-kokoelmat
  • Laskurit ja aikaleiman tyyppiset metatiedot
  • Mikä tahansa sisäinen tila, joka ei vaikuta käyttöliittymään

Lasketut ominaisuudet seurataan automaattisesti

Tämä on rehellisesti sanottuna yksi @Observable-makron hienoimmista ominaisuuksista. Lasketut ominaisuudet (computed properties) seurataan automaattisesti, kun ne riippuvat seuratuista tallennetuista ominaisuuksista:

@Observable
class OstoskoriViewModel {
    var tuotteet: [OstoskoriTuote] = []
    var alennuskoodi: String? = nil
    
    // Seurataan automaattisesti, koska riippuu
    // tuotteista ja alennuskoodista
    var kokonaissumma: Double {
        let summa = tuotteet.reduce(0) { $0 + $1.hinta * Double($1.maara) }
        guard let koodi = alennuskoodi else { return summa }
        return sovellaalennus(summa, koodi: koodi)
    }
    
    var tuotteidenMaara: Int {
        tuotteet.reduce(0) { $0 + $1.maara }
    }
    
    var onTyhja: Bool {
        tuotteet.isEmpty
    }
}

Näkymä, joka lukee pelkän kokonaissumma-ominaisuuden, päivittyy kun tuotteet tai alennuskoodi muuttuu — mutta ei muiden ominaisuuksien muuttuessa. Vanhalla ObservableObject-mallilla tämän toteuttaminen oli todella työlästä.

Vaiheittainen siirtymäopas: ObservableObjectista @Observableen

Okei, teoria on hallussa. Katsotaan miten siirtymä käytännössä tehdään. Tässä vaiheittainen opas.

Vaihe 1: Muunna luokkamäärittely

// Ennen
class AppTila: ObservableObject {
    @Published var onKirjautunut = false
    @Published var kayttajanimi = ""
    @Published var ilmoitukset: [Ilmoitus] = []
}

// Jälkeen
@Observable
class AppTila {
    var onKirjautunut = false
    var kayttajanimi = ""
    var ilmoitukset: [Ilmoitus] = []
}

Vaihe 2: Päivitä property wrapperit näkymissä

// Ennen
struct PaaNakyma: View {
    @StateObject private var appTila = AppTila()
    
    var body: some View {
        LapsiNakyma(tila: appTila)
            .environmentObject(appTila)
    }
}

struct LapsiNakyma: View {
    @ObservedObject var tila: AppTila
    
    var body: some View { /* ... */ }
}

struct SyvaNakyma: View {
    @EnvironmentObject var tila: AppTila
    
    var body: some View { /* ... */ }
}

// Jälkeen
struct PaaNakyma: View {
    @State private var appTila = AppTila()
    
    var body: some View {
        LapsiNakyma(tila: appTila)
            .environment(appTila)
    }
}

struct LapsiNakyma: View {
    var tila: AppTila  // Ei wrapperia
    
    var body: some View { /* ... */ }
}

struct SyvaNakyma: View {
    @Environment(AppTila.self) private var tila
    
    var body: some View { /* ... */ }
}

Vaihe 3: Päivitä sidokset @Bindablella

// Ennen
struct LomakeNakyma: View {
    @ObservedObject var tila: AppTila
    
    var body: some View {
        TextField("Käyttäjänimi", text: $tila.kayttajanimi)
    }
}

// Jälkeen
struct LomakeNakyma: View {
    @Bindable var tila: AppTila
    
    var body: some View {
        TextField("Käyttäjänimi", text: $tila.kayttajanimi)
    }
}

Vaihe 4: Merkitse tarkkailun ulkopuolelle jätettävät

@Observable
class AppTila {
    var onKirjautunut = false
    var kayttajanimi = ""
    
    @ObservationIgnored
    private var cancellables = Set<AnyCancellable>()
    
    @ObservationIgnored
    var sisainenLaskuri = 0
}

Sudenkuopat, joihin jokainen kehittäjä kompastuu

Siirtymä ei ole ihan niin suoraviivainen kuin voisi toivoa. Tässä ovat yleisimmät virheet ja niiden ratkaisut — olen itse kompastunut näistä useampaan kuin yhteen.

Sudenkuoppa 1: @State ei ole sama kuin @StateObject

Tämä on kriittisin ero ymmärtää, ja se yllättää monet.

@StateObject käyttää @autoclosure-parametria, joka alustaa arvon vain kerran. @State puolestaan kutsuu alustajaa joka kerta kun SwiftUI rakentaa näkymän uudelleen — mutta hylkää uuden instanssin ja käyttää välimuistissa olevaa.

Tämä tarkoittaa käytännössä, että @Observable-objektin init-metodia kutsutaan toistuvasti, ja nämä "haamuinstanssit" jäävät muistiin. Jos alustajassa tehdään raskasta työtä — vaikkapa verkkokyselyjä tai ilmoitusten kuuntelua — jokaisesta haamuinstanssista tulee todellinen ongelma.

// VAARALLISTA — älä tee näin!
@Observable
class DataViewModel {
    var data: [Item] = []
    
    init() {
        // Tätä kutsutaan JOKA KERTA kun näkymä
        // rakennetaan uudelleen!
        NotificationCenter.default.addObserver(
            forName: .willTerminate,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.tallennaTiedot()
        }
        lataaDataVerkosta()  // Turha verkkokutsu!
    }
}

// TURVALLISTA — käytä task()-modifieria
@Observable
class DataViewModel {
    var data: [Item] = []
    
    func lataaData() async {
        // Kutsutaan hallitusti task()-modifierista
        data = try await APIClient.haeItems()
    }
}

struct DataNakyma: View {
    @State private var viewModel = DataViewModel()
    
    var body: some View {
        List(viewModel.data) { item in
            Text(item.nimi)
        }
        .task {
            await viewModel.lataaData()
        }
    }
}

Sudenkuoppa 2: Äärettömät uudelleenpiirtosilmukat

Jos luet ja kirjoitat samaa @Observable-ominaisuutta näkymän body-osassa, voit päätyä ikuiseen silmukkaan. Tämä on yksi niistä virheistä, joita on vaikea debugata, koska sovellus vain jumittuu:

// VAARALLISTA — ääretön silmukka!
@Observable
class LaskuriViewModel {
    var laskuri = 0
    var piirtokerrat = 0
}

struct LaskuriNakyma: View {
    @State private var vm = LaskuriViewModel()
    
    var body: some View {
        // body lukee piirtokerrat → tarkkailee sitä
        // body myös MUUTTAA piirtokerrat → uusi piirto
        // → uusi luku → uusi muutos → ikuinen silmukka!
        let _ = { vm.piirtokerrat += 1 }()
        Text("Laskuri: \(vm.laskuri)")
        Text("Piirretty \(vm.piirtokerrat) kertaa")
    }
}

Ratkaisu: älä koskaan muuta tarkkailtavaa ominaisuutta näkymän body-osassa. Käytä @ObservationIgnored sisäisille laskureille tai siirrä logiikka muualle.

Sudenkuoppa 3: @Bindable unohtuu

Siirtymän jälkeen $viewModel.ominaisuus-syntaksi ei toimi ilman @Bindable-wrapperia. Käännösvirheilmoitus ei ole kaikkein intuitiivisin:

// Käännösvirhe!
struct NakymaA: View {
    var viewModel: ProfiiliViewModel
    
    var body: some View {
        TextField("Nimi", text: $viewModel.nimi)
        // ❌ Cannot find '$viewModel' in scope
    }
}

// Korjaus:
struct NakymaA: View {
    @Bindable var viewModel: ProfiiliViewModel
    
    var body: some View {
        TextField("Nimi", text: $viewModel.nimi)
        // ✅ Toimii!
    }
}

Sudenkuoppa 4: @Environment ja @Bindable yhdessä

Kun tarvitset sidoksia ympäristöstä tulevaan @Observable-objektiin, tarvitset hieman erikoisen kuvion:

struct AsetuksetNakyma: View {
    @Environment(AppTila.self) private var appTila
    
    var body: some View {
        // Luo paikallinen @Bindable body-osassa
        @Bindable var tila = appTila
        
        Form {
            TextField("Käyttäjänimi", text: $tila.kayttajanimi)
            Toggle("Ilmoitukset", isOn: $tila.ilmoituksetPaalla)
        }
    }
}

Tämä kuvio voi näyttää oudolta — muuttujan ilmoittaminen suoraan body-ominaisuuden sisällä paikallisena — mutta se on Applen virallinen suositus. Siihen tottuu nopeasti.

withObservationTracking: Edistynyt tarkkailu SwiftUI:n ulkopuolella

SwiftUI käyttää withObservationTracking-funktiota kulissien takana, mutta voit hyödyntää sitä myös itse. Tämä on kätevää esimerkiksi lokitukseen, analytiikkaan tai tilansynkronointiin:

@Observable
class KauppaViewModel {
    var tuotteet: [Tuote] = []
    var valittuKategoria: String? = nil
}

// Tarkkaile muutoksia SwiftUI:n ulkopuolella
func tarkkaileMuutoksia(viewModel: KauppaViewModel) {
    withObservationTracking {
        // Lue ominaisuudet, joita haluat tarkkailla
        let _ = viewModel.valittuKategoria
        let _ = viewModel.tuotteet.count
    } onChange: {
        print("Tuotteet tai kategoria muuttui!")
        // HUOM: Tätä kutsutaan VAIN KERRAN!
        // Tarkkailu on asetettava uudelleen seuraavaa muutosta varten
        tarkkaileMuutoksia(viewModel: viewModel)
    }
}

Tärkeä huomio: withObservationTracking ilmoittaa vain seuraavasta muutoksesta. Jos haluat jatkuvaa tarkkailua, sinun on asetettava se uudelleen onChange-sulkeumassa. Tämä on tarkoituksellinen suunnitteluvalinta, ei bugi.

SE-0506: Edistynyt tarkkailuseuranta (Swift 2026)

Tammikuussa 2026 hyväksytty SE-0506-ehdotus tuo Observation-kehykseen kaksi merkittävää uudistusta kehittyneisiin käyttötapauksiin.

1. Lisävalinnat withObservationTrackingille

Uusi versio withObservationTracking-funktiosta hyväksyy options-parametrin, jolla voidaan hallita tapahtumien tyyppiä:

// Uusi API mahdollistaa tapahtumien tyypin valinnan
withObservationTracking(options: [.willSet, .didSet]) {
    let _ = viewModel.tuotteet
} onChange: { event in
    switch event {
    case .willSet:
        print("Tuotteita ollaan muuttamassa")
    case .didSet:
        print("Tuotteet muuttuivat")
    case .deinit:
        print("Observable vapautettiin")
    }
}

Kolme tapahtumatyyppiä ovat:

  • .willSet — ennen muutosta
  • .didSet — muutoksen jälkeen
  • .deinit — kun tarkkailtava objekti vapautetaan muistista

2. withContinuousObservationTracking

Tässä on hyvä uutinen niille, jotka ovat turhautuneet manuaaliseen uudelleenrekisteröintiin. Uusi jatkuva tarkkailuvariantti hoitaa sen puolestasi:

// Jatkuva tarkkailu — ei tarvetta manuaaliseen uudelleenrekisteröintiin
withContinuousObservationTracking {
    let _ = viewModel.tuotteet
} onChange: {
    print("Tuotteet muuttuivat — kutsutaan automaattisesti uudelleen!")
}

SE-0506 on suunnattu erityisesti kehittyneisiin käyttötapauksiin, kuten middleware-infrastruktuurin tai widgetointijärjestelmien rakentamiseen. Useimmille kehittäjille perus-@Observable-makro riittää edelleen oikein hyvin.

iOS 26: @Observable tulee UIKitiin

Vuoden 2025 WWDC:ssä Apple toi @Observable-tuen suoraan UIKitiin, ja tämä on iso juttu. Se on merkittävä askel kehysten yhdentymisessä.

updateProperties()-metodi

UIKit sai uuden updateProperties()-metodin sekä UIView- että UIViewController-luokissa. Tämä metodi suoritetaan automaattisesti ennen layoutSubviews()-kutsua ja seuraa @Observable-ominaisuuksia automaattisesti:

@Observable
class ProfiiliMalli {
    var nimi: String = "Matti Meikäläinen"
    var kuvaURL: URL? = nil
}

class ProfiiliViewController: UIViewController {
    let malli = ProfiiliMalli()
    let nimiLabel = UILabel()
    
    override func updateProperties() {
        super.updateProperties()
        
        // Seurataan automaattisesti!
        nimiLabel.text = malli.nimi
        // Kun malli.nimi muuttuu, updateProperties()
        // kutsutaan automaattisesti uudelleen
    }
}

Taaksepäin yhteensopivuus iOS 18:aan

Automaattinen tarkkailuseuranta voidaan ottaa käyttöön myös iOS 18:ssa lisäämällä Info.plist-tiedostoon UIObservationTrackingEnabled-avain arvolla YES. Tässä tapauksessa päivityslogiikka sijoitetaan viewWillLayoutSubviews()-metodiin, koska updateProperties() on saatavilla vasta iOS 26:sta alkaen.

Parhaat käytännöt vuodelle 2026

Tiivistetään kaikki opittu konkreettisiksi suosituksiksi:

  1. Uudet projektit: Käytä aina @Observable uusissa iOS 17+ -projekteissa — ei ole syytä käyttää vanhaa mallia
  2. Siirtymästrategia: Älä yritä massamigraatiota. Siirrä yksi näyttö kerrallaan, niin pysyt järjissäsi
  3. Init-metodit: Pidä @Observable-luokkien alustajat kevyinä — ei verkkokyselyjä, ei raskaita operaatioita
  4. @State vain omistajalle: Vain luova näkymä käyttää @State — kaikki muut vastaanottavat ilman wrapperia
  5. @ObservationIgnored: Merkitse kaikki UI:hin vaikuttamattomat ominaisuudet
  6. Pääsäie: Muuta @Observable-ominaisuuksia aina pääsäikeessä käyttöliittymänäkymille
  7. Combine-yhteiselo: @Observable ja Combine voivat elää rinnakkain — merkitse Combine-tilat @ObservationIgnored-makrolla
  8. Testattavuus: @Observable-luokat ovat helposti testattavia ilman SwiftUI-riippuvuutta

Käytännön esimerkki: Kokonainen MVVM-sovellus

Kootaan kaikki opittu yhteen käytännön esimerkkiin. Rakennetaan yksinkertainen tehtävälista-sovellus MVVM-arkkitehtuurilla:

// Model
struct Tehtava: Identifiable {
    let id = UUID()
    var otsikko: String
    var onValmis: Bool = false
    var luotu: Date = .now
}

// ViewModel
@Observable
class TehtavaListaViewModel {
    var tehtavat: [Tehtava] = []
    var uusiTehtavaOtsikko: String = ""
    
    @ObservationIgnored
    private var seuraavaJarjestysNumero = 0
    
    var valmiitTehtavat: [Tehtava] {
        tehtavat.filter { $0.onValmis }
    }
    
    var keskeneraisetTehtavat: [Tehtava] {
        tehtavat.filter { !$0.onValmis }
    }
    
    var edistymisprosentti: Double {
        guard !tehtavat.isEmpty else { return 0 }
        return Double(valmiitTehtavat.count) / Double(tehtavat.count) * 100
    }
    
    func lisaaTehtava() {
        guard !uusiTehtavaOtsikko.isEmpty else { return }
        let tehtava = Tehtava(otsikko: uusiTehtavaOtsikko)
        tehtavat.append(tehtava)
        uusiTehtavaOtsikko = ""
    }
    
    func vaihdaTila(id: UUID) {
        guard let index = tehtavat.firstIndex(where: { $0.id == id }) else { return }
        tehtavat[index].onValmis.toggle()
    }
    
    func poista(id: UUID) {
        tehtavat.removeAll { $0.id == id }
    }
}

// View
struct TehtavaListaNakyma: View {
    @State private var viewModel = TehtavaListaViewModel()
    
    var body: some View {
        NavigationStack {
            VStack {
                EdistymisNakyma(viewModel: viewModel)
                
                LisaaTehtavaNakyma(viewModel: viewModel)
                
                List {
                    ForEach(viewModel.tehtavat) { tehtava in
                        TehtavaRiviNakyma(
                            tehtava: tehtava,
                            onToggle: { viewModel.vaihdaTila(id: tehtava.id) }
                        )
                    }
                    .onDelete { offsets in
                        let idt = offsets.map { viewModel.tehtavat[$0].id }
                        idt.forEach { viewModel.poista(id: $0) }
                    }
                }
            }
            .navigationTitle("Tehtävät")
        }
    }
}

struct EdistymisNakyma: View {
    var viewModel: TehtavaListaViewModel
    // Päivittyy VAIN kun edistymisprosentti muuttuu
    
    var body: some View {
        ProgressView(value: viewModel.edistymisprosentti, total: 100)
            .padding()
    }
}

struct LisaaTehtavaNakyma: View {
    @Bindable var viewModel: TehtavaListaViewModel
    
    var body: some View {
        HStack {
            TextField("Uusi tehtävä...", text: $viewModel.uusiTehtavaOtsikko)
            Button("Lisää", action: viewModel.lisaaTehtava)
                .disabled(viewModel.uusiTehtavaOtsikko.isEmpty)
        }
        .padding(.horizontal)
    }
}

struct TehtavaRiviNakyma: View {
    let tehtava: Tehtava
    let onToggle: () -> Void
    
    var body: some View {
        HStack {
            Image(systemName: tehtava.onValmis ? "checkmark.circle.fill" : "circle")
                .foregroundStyle(tehtava.onValmis ? .green : .gray)
                .onTapGesture(perform: onToggle)
            Text(tehtava.otsikko)
                .strikethrough(tehtava.onValmis)
        }
    }
}

Tässä esimerkissä EdistymisNakyma päivittyy vain edistymisprosentin muuttuessa, LisaaTehtavaNakyma seuraa vain tekstikentän sisältöä, ja jokainen TehtavaRiviNakyma on täysin itsenäinen. Tämä on @Observable-makron voima käytännössä — ja todella mukava tapa rakentaa sovelluksia.

Usein kysytyt kysymykset

Toimiiko @Observable structien kanssa?

Ei valitettavasti. @Observable toimii tällä hetkellä vain luokkien (class) kanssa. Structien tukeminen vaatisi merkittäviä muutoksia Swiftin arvotyyppisemantiikkaan. Käytä structeille edelleen @State-wrapperia suoraan.

Pitääkö kaikki ObservableObject-koodi siirtää kerralla?

Ei todellakaan! ObservableObject ja @Observable voivat elää rinnakkain samassa projektissa. Apple suosittelee vaiheittaista siirtymää: kirjoita uudet ominaisuudet @Observable-mallilla ja migroi vanhaa koodia sitä mukaa kun siihen tulee muutostarpeita.

Miten @Observable vaikuttaa sovelluksen suorituskykyyn?

@Observable parantaa suorituskykyä merkittävästi vähentämällä tarpeettomia näkymäpäivityksiä. Ominaisuustason seuranta tarkoittaa, että vain ne näkymät päivitetään, jotka todella lukevat muuttunutta ominaisuutta. Kymmenientuhansien rivien listassa ero on oikeasti dramaattinen.

Voiko @Observable-objektia käyttää UIKitissä?

Kyllä! iOS 26 tuo @Observable-tuen suoraan UIKitiin uuden updateProperties()-metodin kautta. Automaattinen tarkkailuseuranta voidaan myös ottaa käyttöön iOS 18:ssa UIObservationTrackingEnabled-avaimella Info.plistissä.

Mikä ero on @Bindable- ja @Binding-wrappereilla?

@Binding luo kaksisuuntaisen sidoksen yksittäiseen arvoon ja toimii kaiken tyyppisten arvojen kanssa. @Bindable puolestaan tekee koko @Observable-objektista sidottavan, jolloin voit luoda Binding-arvoja sen ominaisuuksista $-syntaksilla. Toisin sanoen: @Bindable on "portti" Binding-arvoihin @Observable-kontekstissa.

Tietoa Kirjoittajasta Editorial Team

Our team of expert writers and editors.