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 mukaisuudenObservationRegistrar-instanssin, joka hallinnoi tarkkailua- Jokaiselle tallennetulle ominaisuudelle
access- jawithMutation-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:
ObservableObjectiOS 13+ —@ObservableiOS 17+ - Kehys:
ObservableObjectkäyttää Combinea —@Observablekäyttää Observation-kehystä - Ominaisuusmerkintä:
ObservableObjectvaatii@Published—@Observableei vaadi mitään - Omistajuus näkymässä:
@StateObject→@State - Välittäminen:
@ObservedObject→ tavallinenlet/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:
- Uudet projektit: Käytä aina
@Observableuusissa iOS 17+ -projekteissa — ei ole syytä käyttää vanhaa mallia - Siirtymästrategia: Älä yritä massamigraatiota. Siirrä yksi näyttö kerrallaan, niin pysyt järjissäsi
- Init-metodit: Pidä
@Observable-luokkien alustajat kevyinä — ei verkkokyselyjä, ei raskaita operaatioita - @State vain omistajalle: Vain luova näkymä käyttää
@State— kaikki muut vastaanottavat ilman wrapperia - @ObservationIgnored: Merkitse kaikki UI:hin vaikuttamattomat ominaisuudet
- Pääsäie: Muuta
@Observable-ominaisuuksia aina pääsäikeessä käyttöliittymänäkymille - Combine-yhteiselo:
@Observableja Combine voivat elää rinnakkain — merkitse Combine-tilat@ObservationIgnored-makrolla - 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.