Johdanto: Miksi SwiftData muuttaa kaiken
Tietojen pysyvyys on jokaisen vakavasti otettavan iOS-sovelluksen ytimessä. Olipa kyseessä tehtävälista, muistiinpanosovellus tai monimutkainen liiketoimintasovellus, tarvitset luotettavan tavan tallentaa ja hakea dataa. Apple esitteli SwiftDatan WWDC23:ssa korvaamaan Core Datan monimutkaisuuden modernilla, Swiftille natiivilta tuntuvalla ratkaisulla — ja iOS 26:n myötä se on vihdoin kypsä tuotantokäyttöön.
Jos olet lukenut aiemman artikkelimme Swift 6.2:n lähestyttävästä rinnakkaisuudesta, tämä opas on luonteva jatko: kun koodisi rinnakkaisuusrakenne on kunnossa, on aika rakentaa sille vankka tietopohja. Ja jos tutustuit Liquid Glass -oppaamme, SwiftData tarjoaa tietokerroksen niiden kauniiden käyttöliittymien taakse.
Tässä oppaassa käymme läpi SwiftDatan perusteista aina edistyneisiin tekniikoihin asti. Mallien määrittelystä kyselyihin, suhteiden hallinnasta CloudKit-synkronointiin, ja iOS 26:n uudesta malliperinnöstä migraatiostrategioihin. Mukana on runsaasti käytännön koodiesimerkkejä, jotta voit soveltaa oppimaasi suoraan omissa projekteissasi.
SwiftDatan perusteet: @Model, ModelContainer ja ModelContext
SwiftDatan arkkitehtuuri rakentuu kolmen pääkomponentin varaan. Rehellisesti sanottuna, näiden ymmärtäminen on se ensimmäinen ja tärkein askel — ilman näitä ei oikein pääse alkuun.
@Model-makro: Tietomallien määrittely
@Model-makro on SwiftDatan sydän. Se muuttaa tavallisen Swift-luokan pysyvään tallennukseen sopivaksi tietomalliksi automaattisesti. Kääntäjä generoi tarvittavan tallennuslogiikan puolestasi — ei tarvitse kirjoittaa yhtään ylimääräistä boilerplate-koodia.
import SwiftData
@Model
class Projekti {
var nimi: String
var kuvaus: String
var luontipvm: Date
var onValmis: Bool
init(nimi: String, kuvaus: String, luontipvm: Date = .now, onValmis: Bool = false) {
self.nimi = nimi
self.kuvaus = kuvaus
self.luontipvm = luontipvm
self.onValmis = onValmis
}
}
Tämä yksinkertainen merkintä tekee valtavasti työtä konepellin alla. @Model lisää luokalle PersistentModel-protokollan mukaisuuden, generoi skeematiedot jokaiselle ominaisuudelle ja mahdollistaa automaattisen muutosten seurannan. Core Datan aikaiset XML-skeematiedostot? Ne jäivät historiaan.
Tuetut tietotyypit
SwiftData tukee laajaa valikoimaa tietotyyppejä suoraan:
- Perustyypit:
String,Int,Double,Float,Bool,Date,Data,URL,UUID - Kokoelmat:
Array,Dictionary,Set— kunhan elementit ovatCodable-yhteensopivia - Valinnaistyypit: mikä tahansa yllä olevista
Optional-kääreenä - Enumit:
Codable-yhteensopivat enumeraatiot - Codable-tyypit: omat
struct-rakenteet, jotka toteuttavatCodable-protokollan
ModelContainer: Tallennuksen konfigurointi
ModelContainer on pysyvyystallennuksen ydin. Se hallinnoi skeemaa, konfiguroi tallennuspaikan ja koordinoi tietokantaoperaatiot.
Jokainen SwiftData-sovellus tarvitsee vähintään yhden.
import SwiftUI
import SwiftData
@main
struct ProjektienHallintaApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Projekti.self, Tehtava.self])
}
}
Tämä yksinkertainen rivi luo SQLite-tietokannan sovelluksen dokumenttikansioon ja rekisteröi kaikki määritellyt mallit. Mutta hei — ModelContainer tarjoaa paljon enemmän konfigurointivaihtoehtoja kuin pelkkä perusversio:
let konfiguraatio = ModelConfiguration(
"ProjektitDB",
schema: Schema([Projekti.self, Tehtava.self]),
isStoredInMemoryOnly: false,
allowsSave: true,
groupContainer: .identifier("group.com.oma.sovellus")
)
let container = try ModelContainer(
for: Projekti.self, Tehtava.self,
configurations: konfiguraatio
)
Tässä voit määrittää tietokannan nimen, käyttää App Group -jakoa (esimerkiksi widgetin ja pääsovelluksen välillä), rajoittaa tallennusta vain muistiin (hyödyllistä esikatseluissa ja testeissä) tai jopa estää tallentamisen kokonaan. Varsinkin tuo App Group -jako on kultaakin arvokkaampi, jos sovelluksessasi on widget.
ModelContext: Tietokantaoperaatioiden hallinta
ModelContext on käyttöliittymäsi tietokantaan. Se vastaa tietojen hakemisesta, lisäämisestä, poistamisesta ja muutosten tallentamisesta. Ajattele sitä työpöytänä: teet muutoksia muistissa oleviin objekteihin ja tallennat ne sitten levylle.
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
func lisaaProjekti() {
let uusiProjekti = Projekti(
nimi: "Uusi projekti",
kuvaus: "Projektin kuvaus tähän"
)
modelContext.insert(uusiProjekti)
// SwiftData tallentaa automaattisesti kun on sopiva hetki
}
func poistaProjekti(_ projekti: Projekti) {
modelContext.delete(projekti)
}
}
Tärkeä yksityiskohta: SwiftData tallentaa muutokset automaattisesti tietyissä tilanteissa — kun käyttäjä navigoi pois näkymästä, kun sovellus siirtyy taustalle tai kun järjestelmä katsoo sen tarpeelliseksi. Voit myös tallentaa manuaalisesti kutsumalla try modelContext.save(), mutta useimmiten automaattinen tallennus riittää mainiosti.
Kyselyt ja suodatus: @Query ja FetchDescriptor
Tietojen hakeminen tietokannasta on yksi yleisimmistä operaatioista, ja tässä SwiftData loistaa. Käytettävissä on kaksi päämekanismia: @Query-makro SwiftUI-näkymissä ja FetchDescriptor-luokka ohjelmalliseen käyttöön.
@Query-makro: Reaktiiviset kyselyt
@Query on SwiftUI-property wrapper, joka hakee tietoja tietokannasta ja päivittää näkymän automaattisesti kun tiedot muuttuvat. Jos olet käyttänyt Core Datan @FetchRequest-wrapperiä, tämä tuntuu tutulta — mutta huomattavasti siistimmältä.
struct ProjektiListaView: View {
@Query(
filter: #Predicate { !$0.onValmis },
sort: \Projekti.luontipvm,
order: .reverse
)
private var keskeneraisetProjektit: [Projekti]
var body: some View {
List(keskeneraisetProjektit) { projekti in
VStack(alignment: .leading) {
Text(projekti.nimi)
.font(.headline)
Text(projekti.kuvaus)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
Huomaa #Predicate-makron käyttö suodatuksessa. Se on tyyppiturvallinen ja kääntäjä tarkistaa sen oikeellisuuden. Et voi tehdä kirjoitusvirheitä ominaisuuksien nimissä — kääntäjä yksinkertaisesti estää sen. Tämä on yksi niistä asioista, joiden vuoksi SwiftData tuntuu niin paljon mukavammalta kuin edeltäjänsä.
Monimutkaisten predikaattien rakentaminen
Predikaatit voivat olla huomattavasti monimutkaisempia kuin yksinkertainen vertailu. Ehtoja voi yhdistellä melko vapaasti:
// Hae projektit, jotka sisältävät hakusanan ja jotka luotiin viimeisen kuukauden aikana
let hakusana = "Swift"
let kuukausiSitten = Calendar.current.date(byAdding: .month, value: -1, to: .now)!
@Query(
filter: #Predicate { projekti in
projekti.nimi.localizedStandardContains(hakusana) &&
projekti.luontipvm > kuukausiSitten &&
!projekti.onValmis
},
sort: [
SortDescriptor(\Projekti.luontipvm, order: .reverse),
SortDescriptor(\Projekti.nimi)
]
)
private var suodatetutProjektit: [Projekti]
Ja tässä tulee iOS 26:n iloinen uutinen: nyt voit käyttää myös Codable-ominaisuuksia predikaateissa. Aiemmin tämä ei ollut mahdollista, ja se oli rehellisesti sanottuna yksi SwiftDatan turhauttavimmista rajoituksista.
FetchDescriptor: Ohjelmallinen haku
Kun tarvitset tietoja SwiftUI-näkymän ulkopuolella — esimerkiksi view modelissa tai palveluluokassa — FetchDescriptor on oikea työkalu:
func haeValmistuneetProjektit() throws -> [Projekti] {
let descriptor = FetchDescriptor(
predicate: #Predicate { $0.onValmis },
sortBy: [SortDescriptor(\Projekti.luontipvm, order: .reverse)]
)
return try modelContext.fetch(descriptor)
}
// Sivutus suurille tietomäärille
func haeProjektitSivuittain(sivu: Int, sivunkoko: Int = 20) throws -> [Projekti] {
var descriptor = FetchDescriptor(
sortBy: [SortDescriptor(\Projekti.luontipvm, order: .reverse)]
)
descriptor.fetchOffset = sivu * sivunkoko
descriptor.fetchLimit = sivunkoko
return try modelContext.fetch(descriptor)
}
// Pelkkä lukumäärän hakeminen
func laskeKeskeneraiset() throws -> Int {
let descriptor = FetchDescriptor(
predicate: #Predicate { !$0.onValmis }
)
return try modelContext.fetchCount(descriptor)
}
FetchDescriptor tukee myös fetchOffset- ja fetchLimit-ominaisuuksia sivutusta varten. Tämä on kriittistä erityisesti silloin, kun tietokannassa on tuhansia rivejä — et todellakaan halua ladata kaikkia muistiin kerralla.
Dynaamiset kyselyt
Joskus tarvitset kyselyitä, joiden suodatuskriteerit muuttuvat käyttäjän toiminnan perusteella. SwiftUI:n kanssa tämä onnistuu luomalla erillinen alanäkymä, joka vastaanottaa suodatusparametrit:
struct DynaaminenProjektiLista: View {
@Query private var projektit: [Projekti]
init(naytaVainKeskeneraiset: Bool, jarjestys: SortDescriptor) {
let suodatin: Predicate? = naytaVainKeskeneraiset
? #Predicate { !$0.onValmis }
: nil
_projektit = Query(
filter: suodatin,
sort: [jarjestys]
)
}
var body: some View {
List(projektit) { projekti in
ProjektiRiviView(projekti: projekti)
}
}
}
struct PaaView: View {
@State private var naytaVainKeskeneraiset = false
var body: some View {
DynaaminenProjektiLista(
naytaVainKeskeneraiset: naytaVainKeskeneraiset,
jarjestys: SortDescriptor(\Projekti.nimi)
)
.toolbar {
Toggle("Vain keskeneräiset", isOn: $naytaVainKeskeneraiset)
}
}
}
Suhteet ja tietomallin suunnittelu
Todellisissa sovelluksissa tiedot eivät koskaan elä eristyksissä. Projektilla on tehtäviä, käyttäjällä on tilauksia, blogilla on artikkeleita ja tageja. SwiftData tukee yksi-moneen ja monta-moneen -suhteita natiivisti, ja onneksi niiden määrittely on varsin suoraviivaista.
Yksi-moneen-suhde
Yleisin suhdetyyppi on yksi-moneen. Projektilla voi olla useita tehtäviä, mutta jokainen tehtävä kuuluu yhteen projektiin:
@Model
class Projekti {
var nimi: String
var kuvaus: String
var luontipvm: Date
var onValmis: Bool
@Relationship(deleteRule: .cascade, inverse: \Tehtava.projekti)
var tehtavat: [Tehtava] = []
init(nimi: String, kuvaus: String, luontipvm: Date = .now, onValmis: Bool = false) {
self.nimi = nimi
self.kuvaus = kuvaus
self.luontipvm = luontipvm
self.onValmis = onValmis
}
}
@Model
class Tehtava {
var otsikko: String
var onTehty: Bool
var prioriteetti: Int
var projekti: Projekti?
init(otsikko: String, onTehty: Bool = false, prioriteetti: Int = 0) {
self.otsikko = otsikko
self.onTehty = onTehty
self.prioriteetti = prioriteetti
}
}
@Relationship-attribuutin deleteRule-parametri määrittää, mitä lapsiobjekteille tapahtuu vanhemman poistuessa:
.cascade— Poistaa kaikki liittyvät objektit. Kun projekti poistetaan, kaikki sen tehtävätkin lähtevät. Tämä on usein se mitä haluat vanhempi-lapsi-suhteissa..nullify— Asettaa viittauksennil-arvoon. Tehtävä jää olemaan, mutta senprojekti-ominaisuus muuttuunil:ksi. Tämä on oletusarvo..deny— Estää poistamisen kokonaan, jos liittyviä objekteja on olemassa..noAction— Ei tee mitään. Voi johtaa orpoihin tietueisiin, joten käytä tätä varoen.
Pieni varoituksen sana: Cascade-poistosääntö toimii luotettavasti vain, kun automaattinen tallennus on käytössä. Jos olet poistanut automaattisen tallennuksen käytöstä, cascade-poisto saattaa tehdä yllättäviä asioita — tai jättää tekemättä mitään. Tämä on tunnettu ongelma, jonka kanssa moni kehittäjä on kamppaillut.
Monta-moneen-suhde
Monta-moneen-suhteita tarvitaan esimerkiksi tagien tai kategorioiden kanssa. Tässä klassinen esimerkki:
@Model
class Artikkeli {
var otsikko: String
var sisalto: String
@Relationship(inverse: \Tagi.artikkelit)
var tagit: [Tagi] = []
init(otsikko: String, sisalto: String) {
self.otsikko = otsikko
self.sisalto = sisalto
}
}
@Model
class Tagi {
var nimi: String
var artikkelit: [Artikkeli] = []
init(nimi: String) {
self.nimi = nimi
}
}
// Käyttö:
let swift = Tagi(nimi: "Swift")
let swiftui = Tagi(nimi: "SwiftUI")
let artikkeli = Artikkeli(
otsikko: "SwiftUI-vinkkejä",
sisalto: "..."
)
artikkeli.tagit = [swift, swiftui]
Attribuuttien konfigurointi
@Attribute-makro tarjoaa lisäkonfigurointia yksittäisille ominaisuuksille. Näitä kannattaa tuntea, sillä ne ovat käytännössä välttämättömiä oikeissa projekteissa:
@Model
class Kayttaja {
@Attribute(.unique) var sahkoposti: String // Uniikki rajoite
var nimi: String
@Attribute(.externalStorage) var profiilikuva: Data? // Tallennetaan erillisenä tiedostona
@Attribute(.spotlight) var esittely: String? // Indeksoidaan Spotlight-hakua varten
@Transient var valiaikainenTieto: String = "" // Ei tallenneta tietokantaan
init(sahkoposti: String, nimi: String) {
self.sahkoposti = sahkoposti
self.nimi = nimi
}
}
Huomaa erityisesti @Transient-merkintä — se kertoo SwiftDatalle, ettei kyseistä ominaisuutta tule tallentaa. Tämä on hyödyllistä väliaikaisille lasketuille arvoille tai tilatiedoille, joiden ei pidä päätyä tietokantaan. Olen itse käyttänyt tätä esimerkiksi validointitilojen ja väliaikaisten käyttöliittymätilojen tallentamiseen.
iOS 26:n uutuus: Malliperinnöllisyys
Nyt päästään siihen osaan, josta olen aidosti innoissani. WWDC25:ssä esitelty malliperinnöllisyys on SwiftDatan merkittävin uusi ominaisuus, ja se on ominaisuus jota kehittäjät ovat toivoneet ihan ensimmäisestä SwiftData-versiosta lähtien.
Perinnöllisyyden perusteet
Malliperinnöllisyys on luonteva valinta silloin, kun sinulla on useita mallityyppejä jotka jakavat yhteisiä ominaisuuksia mutta eroavat tietyiltä osin:
@Model
class Matka {
var kohde: String
var alkupvm: Date
var loppupvm: Date
var muistiinpanot: String
init(kohde: String, alkupvm: Date, loppupvm: Date, muistiinpanot: String = "") {
self.kohde = kohde
self.alkupvm = alkupvm
self.loppupvm = loppupvm
self.muistiinpanot = muistiinpanot
}
}
@available(iOS 26, *)
@Model
class Tyomatka: Matka {
var paivaraha: Double
var yritys: String
var kokousaiheet: [String]
init(kohde: String, alkupvm: Date, loppupvm: Date, paivaraha: Double, yritys: String, kokousaiheet: [String] = []) {
self.paivaraha = paivaraha
self.yritys = yritys
self.kokousaiheet = kokousaiheet
super.init(kohde: kohde, alkupvm: alkupvm, loppupvm: loppupvm)
}
}
@available(iOS 26, *)
@Model
class Lomamatka: Matka {
var syy: LomaSyy
var budjetti: Double
init(kohde: String, alkupvm: Date, loppupvm: Date, syy: LomaSyy, budjetti: Double) {
self.syy = syy
self.budjetti = budjetti
super.init(kohde: kohde, alkupvm: alkupvm, loppupvm: loppupvm)
}
}
enum LomaSyy: String, Codable {
case rentoutuminen
case seikkailu
case kulttuuri
case perheloma
}
Huomaa @available(iOS 26, *) -merkintä aliluokissa. Tämä on pakollinen, koska malliperinnöllisyys vaatii iOS 26:n tai uudemman version.
ModelContainerin konfigurointi
Kun käytät malliperinnöllisyyttä, sinun täytyy rekisteröidä sekä yliluokka että aliluokat ModelContainer-konfiguraatiossa:
@main
struct MatkasovelluksApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Matka.self, Tyomatka.self, Lomamatka.self])
}
}
Kyselyt perinnöllisyyden kanssa
Tässä perinnöllisyys alkaa todella näyttää voimansa. Voit hakea kaikki matkat riippumatta tyypistä tai suodattaa tiettyyn alityyppiin:
// Kaikki matkat — sekä työ- että lomamatkat
@Query var kaikkiMatkat: [Matka]
// Pelkästään työmatkat
@Query var tyomatkat: [Tyomatka]
// Pelkästään lomamatkat tietyllä budjetilla
@Query(
filter: #Predicate { $0.budjetti < 2000 }
)
var edullisetLomat: [Lomamatka]
Tämä on merkittävä etu verrattuna vaihtoehtoisiin ratkaisuihin, kuten enum-pohjaiseen tyyppierotteluun tai erillisiin malleihin. Koodi pysyy selkeänä ja kyselyt tuntuvat luontevilta.
Arkkitehtuurimallit ja parhaat käytännöt
SwiftDatan suunnitteluperiaatteet kannustavat suoraviivaiseen käyttöön, mutta isommissa projekteissa selkeä arkkitehtuuri on ehdottoman välttämätön. Luota minuun — olen nähnyt projekteja, joissa tietokantalogiikka on ripoteltu sinne tänne näkymiin, ja lopputulos ei ole kaunis.
Repository-malli: Tietokantakerroksen abstrahointi
Vaikka SwiftData on suunniteltu käytettäväksi suoraan näkymissä @Query-makron avulla, monimutkaisemmissa sovelluksissa erillinen tietokantakerros on lähes aina järkevä ratkaisu:
protocol ProjektiRepository {
func haeKaikki() throws -> [Projekti]
func haeKeskeneraiset() throws -> [Projekti]
func haeNimella(_ nimi: String) throws -> [Projekti]
func lisaa(_ projekti: Projekti) throws
func poista(_ projekti: Projekti) throws
func tallenna() throws
}
class SwiftDataProjektiRepository: ProjektiRepository {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func haeKaikki() throws -> [Projekti] {
let descriptor = FetchDescriptor(
sortBy: [SortDescriptor(\Projekti.luontipvm, order: .reverse)]
)
return try modelContext.fetch(descriptor)
}
func haeKeskeneraiset() throws -> [Projekti] {
let descriptor = FetchDescriptor(
predicate: #Predicate { !$0.onValmis },
sortBy: [SortDescriptor(\Projekti.luontipvm, order: .reverse)]
)
return try modelContext.fetch(descriptor)
}
func haeNimella(_ nimi: String) throws -> [Projekti] {
let descriptor = FetchDescriptor(
predicate: #Predicate { projekti in
projekti.nimi.localizedStandardContains(nimi)
}
)
return try modelContext.fetch(descriptor)
}
func lisaa(_ projekti: Projekti) throws {
modelContext.insert(projekti)
try tallenna()
}
func poista(_ projekti: Projekti) throws {
modelContext.delete(projekti)
try tallenna()
}
func tallenna() throws {
try modelContext.save()
}
}
Tämä lähestymistapa tuo mukanaan useita etuja: se erottaa tietokantalogiikan käyttöliittymästä, mahdollistaa yksikkötestauksen mock-toteutuksilla ja helpottaa mahdollista siirtymää toiseen tallennusratkaisuun tulevaisuudessa. Se myös tekee koodista helpommin luettavaa kollegoille (tai itsellesi kuukauden päästä).
Validointi mallissa
Validointilogiikka kuuluu malliin, ei näkymään. Tämä tekee koodista uudelleenkäytettävää ja testattavaa:
@Model
class Projekti {
var nimi: String
var kuvaus: String
var luontipvm: Date
var onValmis: Bool
@Relationship(deleteRule: .cascade, inverse: \Tehtava.projekti)
var tehtavat: [Tehtava] = []
var onValidi: Bool {
!nimi.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
nimi.count <= 100 &&
kuvaus.count <= 500
}
var keskeneraistenTehtavienMaara: Int {
tehtavat.filter { !$0.onTehty }.count
}
var edistymisprosentti: Double {
guard !tehtavat.isEmpty else { return 0 }
let valmiit = tehtavat.filter { $0.onTehty }.count
return Double(valmiit) / Double(tehtavat.count) * 100
}
init(nimi: String, kuvaus: String, luontipvm: Date = .now, onValmis: Bool = false) {
self.nimi = nimi
self.kuvaus = kuvaus
self.luontipvm = luontipvm
self.onValmis = onValmis
}
}
Esikatselut ja testaus
SwiftUI-esikatseluissa SwiftData-mallit vaativat muistinsisäisen konttien käyttöä. Tämä on tärkeää, jotta testidataa ei päädy oikeaan tietokantaan:
struct ProjektiNakymaEsikatselut {
static var esikatseluContainer: ModelContainer {
let konfiguraatio = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(
for: Projekti.self, Tehtava.self,
configurations: konfiguraatio
)
// Lisää esikatseludataa
let konteksti = container.mainContext
let esimerkki = Projekti(nimi: "Esimerkkiprojekti", kuvaus: "Kuvaus tähän")
konteksti.insert(esimerkki)
let tehtava1 = Tehtava(otsikko: "Ensimmäinen tehtävä", onTehty: true)
let tehtava2 = Tehtava(otsikko: "Toinen tehtävä")
tehtava1.projekti = esimerkki
tehtava2.projekti = esimerkki
return container
}
}
#Preview {
ProjektiListaView()
.modelContainer(ProjektiNakymaEsikatselut.esikatseluContainer)
}
@ModelActor: Taustakontekstin hallinta
@ModelActor on SwiftDatan ratkaisu taustasäikeisiin tietokantaoperaatioihin. Se luo oman aktoriympäristön omalla ModelContext-instanssillaan, mikä mahdollistaa raskaat operaatiot pääsäikeen ulkopuolella. Tämä on ehdottoman tärkeää, jos sovelluksesi käsittelee suurempia datamääriä.
@ModelActor
actor TietojenTuontipalvelu {
func tuoProjektitJSONsta(_ data: Data) throws -> Int {
let dekooderi = JSONDecoder()
dekooderi.dateDecodingStrategy = .iso8601
let projektitDTO = try dekooderi.decode([ProjektiDTO].self, from: data)
for dto in projektitDTO {
let projekti = Projekti(
nimi: dto.nimi,
kuvaus: dto.kuvaus,
luontipvm: dto.luontipvm
)
modelContext.insert(projekti)
for tehtavaDTO in dto.tehtavat {
let tehtava = Tehtava(
otsikko: tehtavaDTO.otsikko,
onTehty: tehtavaDTO.onTehty,
prioriteetti: tehtavaDTO.prioriteetti
)
tehtava.projekti = projekti
modelContext.insert(tehtava)
}
}
try modelContext.save()
return projektitDTO.count
}
}
// Käyttö näkymässä
struct TuontiView: View {
@Environment(\.modelContext) private var modelContext
@State private var tuodaanko = false
var body: some View {
Button("Tuo projektit") {
tuodaanko = true
Task {
let palvelu = TietojenTuontipalvelu(
modelContainer: modelContext.container
)
let maara = try await palvelu.tuoProjektitJSONsta(jsonData)
tuodaanko = false
}
}
.disabled(tuodaanko)
}
}
Hyvä uutinen: iOS 26:n myötä aiempi ärsyttävä bugi, joka esti näkymäpäivitykset @ModelActor-kontekstissa tehdyistä muutoksista, on vihdoin korjattu. Tämä tekee taustakontekstien käytöstä oikeasti luotettavaa tuotantosovelluksissa.
CloudKit-synkronointi
Yksi SwiftDatan parhaista ominaisuuksista on saumaton integraatio CloudKitin kanssa. Se mahdollistaa tietojen automaattisen synkronoinnin käyttäjän laitteiden välillä ilman yhtään palvelinpuolen koodia. Kuulostaa liian hyvältä ollakseen totta? No, melkein.
Perusasetukset
CloudKit-synkronoinnin käyttöönotto vaatii muutaman vaiheen:
- Avaa projektin Signing & Capabilities -asetukset
- Lisää iCloud-kyvykkyys
- Valitse CloudKit vaihtoehdoista
- Luo uusi CloudKit-kontti painamalla +
- Lisää Background Modes -kyvykkyys
- Aktivoi Remote Notifications
SwiftData-koodissa itsessään ei tarvita juuri mitään muutoksia — synkronointi käynnistyy automaattisesti, kun iCloud-kontti on konfiguroitu oikein. Tämä on yksi niistä asioista, jotka SwiftData tekee todella hyvin.
CloudKit-yhteensopivuuden vaatimukset
CloudKit asettaa tietomallille kuitenkin tiettyjä rajoituksia, jotka on ehdottomasti hyvä tietää etukäteen (ennen kuin törmäät niihin tuotannossa):
- Ei uniikkeja rajoitteita:
@Attribute(.unique)ei toimi CloudKit-synkronoinnin kanssa. Jos tarvitset uniikkeja arvoja, joudut toteuttamaan tarkistuksen manuaalisesti. - Oletusarvot pakollisia: Kaikkien ominaisuuksien täytyy olla joko valinnaisia (
Optional) tai niillä täytyy olla oletusarvo. - Suhteet valinnaisia: Kaikkien suhteiden (
@Relationship) täytyy olla valinnaisia. - Vain kevyet migraatiot: Kun sovellus on julkaistu, skeemamuutokset rajoittuvat kevyisiin migraatioihin — uusia ominaisuuksia ja entiteettejä voi lisätä, mutta olemassa olevia ei voi poistaa tai muokata.
// CloudKit-yhteensopiva malli
@Model
class CloudProjekti {
var nimi: String = "" // Oletusarvo
var kuvaus: String = "" // Oletusarvo
var luontipvm: Date = .now // Oletusarvo
var onValmis: Bool = false // Oletusarvo
@Relationship(deleteRule: .cascade)
var tehtavat: [CloudTehtava]? = [] // Valinnainen suhde
init(nimi: String, kuvaus: String) {
self.nimi = nimi
self.kuvaus = kuvaus
}
}
Skeeman alustaminen
Jos synkronoinnissa esiintyy ongelmia — osittaista datakatoa tai synkronoimattomia suhteita — syy liittyy usein siihen, ettei pilven skeema vastaa paikallista mallia. Tähän on onneksi yksinkertainen ratkaisu:
#if DEBUG
do {
try container.initializeCloudKitSchema()
} catch {
print("CloudKit-skeeman alustus epäonnistui: \(error)")
}
#endif
Tämä pakottaa CloudKitin päivittämään skeemansa vastaamaan paikallista tietomallia. Käytä tätä vain kehitysaikana — tuotantosovelluksessa sen kutsuminen ei ole tarpeen eikä suotavaa.
Migraatiostrategiat
Kun tietomalli muuttuu sovelluksen elinkaaren aikana (ja kyllä, se muuttuu aina), tarvitset migraatiostrategian olemassa olevan datan säilyttämiseksi.
Kevyt migraatio
SwiftData suorittaa kevyen migraation automaattisesti, kun muutokset ovat riittävän yksinkertaisia: uusien ominaisuuksien lisääminen oletusarvoilla, ominaisuuksien tekeminen valinnaisiksi tai uusien mallien lisääminen. Sinun ei tarvitse kirjoittaa mitään migraatiokoodia näissä tapauksissa. Kätevää.
Vaiheistettu migraatio (SchemaMigrationPlan)
Monimutkaisemmissa muutoksissa — kuten ominaisuuksien uudelleennimeämisessä tai tietojen muuntamisessa — tarvitset SchemaMigrationPlan-luokkaa:
enum ProjektiMigraatio: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[ProjektiSkemaV1.self, ProjektiSkemaV2.self]
}
static var stages: [MigrationStage] {
[migraatioV1V2]
}
static let migraatioV1V2 = MigrationStage.custom(
fromVersion: ProjektiSkemaV1.self,
toVersion: ProjektiSkemaV2.self
) { context in
// Muunna olemassa oleva data
let vanhatProjektit = try context.fetch(
FetchDescriptor()
)
for projekti in vanhatProjektit {
// Esimerkki: lisää uusi laskennallinen arvo
projekti.slug = projekti.nimi
.lowercased()
.replacingOccurrences(of: " ", with: "-")
}
try context.save()
}
}
enum ProjektiSkemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Projekti.self]
}
@Model
class Projekti {
var nimi: String
var kuvaus: String
init(nimi: String, kuvaus: String) {
self.nimi = nimi
self.kuvaus = kuvaus
}
}
}
enum ProjektiSkemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Projekti.self]
}
@Model
class Projekti {
var nimi: String
var kuvaus: String
var slug: String // Uusi ominaisuus
init(nimi: String, kuvaus: String, slug: String = "") {
self.nimi = nimi
self.kuvaus = kuvaus
self.slug = slug
}
}
}
Migraatiosuunnitelma rekisteröidään ModelContainer-konfiguraatiossa:
.modelContainer(
for: Projekti.self,
migrationPlan: ProjektiMigraatio.self
)
Kumoa ja toista (Undo/Redo)
SwiftData tukee sisäänrakennettua kumoa/toista-toiminnallisuutta, ja se on yllättävän helppo ottaa käyttöön:
@main
struct ProjektienHallintaApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Projekti.self, isUndoEnabled: true)
}
}
struct ProjektiMuokkausView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.undoManager) private var undoManager
@Bindable var projekti: Projekti
var body: some View {
Form {
TextField("Nimi", text: $projekti.nimi)
TextField("Kuvaus", text: $projekti.kuvaus)
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Kumoa") {
undoManager?.undo()
}
.disabled(!(undoManager?.canUndo ?? false))
}
ToolbarItem(placement: .topBarTrailing) {
Button("Toista") {
undoManager?.redo()
}
.disabled(!(undoManager?.canRedo ?? false))
}
}
}
}
Kun isUndoEnabled on päällä, jokainen tietomalliin tehty muutos rekisteröidään automaattisesti kumoa-historiaan. Käyttäjä voi kumota muutoksia painikkeilla, pikanäppäimillä tai iPhonessa jopa ravistamalla laitetta. Pienellä vaivalla saat valtavan käyttökokemushyödyn.
Suorituskyvyn optimointi
SwiftData on suunniteltu suorituskykyiseksi oletusarvoisesti, mutta suurilla tietomäärillä optimointi muuttuu kriittiseksi. Tässä muutama keskeinen tekniikka.
Sivutus ja lazy-lataus
func haeSivutettuLista(
sivunumero: Int,
sivunkoko: Int = 50
) throws -> [Projekti] {
var descriptor = FetchDescriptor(
sortBy: [SortDescriptor(\Projekti.luontipvm, order: .reverse)]
)
descriptor.fetchLimit = sivunkoko
descriptor.fetchOffset = sivunumero * sivunkoko
return try modelContext.fetch(descriptor)
}
Eräkäsittely suurille datamäärille
Kun tuot suuria tietomääriä, tallentaminen eräajona auttaa välttämään muistiongelmia:
@ModelActor
actor MassatuontiPalvelu {
func tuoSuuriDatajoukko(_ kohteet: [ProjektiDTO]) throws {
let erakoko = 100
for eraIndeksi in stride(from: 0, to: kohteet.count, by: erakoko) {
let eranLoppu = min(eraIndeksi + erakoko, kohteet.count)
let era = kohteet[eraIndeksi..
Muistinkäytön hallinta
SwiftData pitää haettuja objekteja muistissa ModelContext-instanssin elinkaaren ajan. Suurilla tietomäärillä tämä voi johtaa muistiongelmiin. Käytännön ratkaisuja ovat sivutetut kyselyt, erillisten kontekstien käyttö lyhytaikaisiin operaatioihin ja tarpeettomien objektien vapauttaminen kutsumalla modelContext.reset().
SwiftData vs. Core Data: Kumpi valita?
Tämä kysymys nousee esiin jokaisessa iOS-kehittäjäyhteisössä, ja se on hyvä kysymys. Tässä tiivistetty näkemykseni:
- Uusi projekti, iOS 17+: Valitse SwiftData. Se on modernimpi, Swiftille natiivi ja selkeästi Applen tulevaisuuden suunta.
- Olemassa oleva Core Data -projekti: Ei kiirettä siirtyä. Core Data on edelleen täysin tuettu ja vakaa. Migraatio SwiftDataan on mahdollinen, mutta harvoin välttämätön.
- Monimutkainen tietomalli: Core Data tarjoaa edelleen enemmän kontrollia ja joustavuutta. SwiftDatan rajoitukset — erityisesti CloudKit-yhteensopivuudessa — voivat aiheuttaa päänvaivaa.
- iOS 26+: SwiftData on ensisijainen valinta. Malliperinnöllisyys, parannetut predikaatit ja korjatut bugit tekevät siitä vihdoin tuotantovalmiin.
Oleellista on muistaa, että SwiftData ja Core Data käyttävät samaa tallennusmoottoria (SQLite) ja jakavat monia sisäisiä mekanismeja. SwiftData on käytännössä Core Datan moderni käyttöliittymä. Voit jopa käyttää molempia samassa projektissa rinnakkain, jos tarve vaatii.
Käytännön esimerkki: Projektinhallintasovellus
Eli kootaan kaikki yhteen yhdellä käytännön esimerkillä. Rakennetaan yksinkertainen projektinhallintasovellus, joka hyödyntää kaikkia edellä käsiteltyjä ominaisuuksia:
import SwiftUI
import SwiftData
// Sovelluksen päärakenne
@main
struct ProjektienHallintaApp: App {
var body: some Scene {
WindowGroup {
ProjektiNavigaatioView()
}
.modelContainer(
for: [Projekti.self, Tehtava.self],
isUndoEnabled: true
)
}
}
// Päänäkymä navigaatiolla
struct ProjektiNavigaatioView: View {
@Environment(\.modelContext) private var modelContext
@Query(
sort: \Projekti.luontipvm,
order: .reverse
)
private var projektit: [Projekti]
@State private var hakuteksti = ""
@State private var naytaVainKeskeneraiset = false
var suodatetutProjektit: [Projekti] {
projektit.filter { projekti in
let vastaaHakua = hakuteksti.isEmpty ||
projekti.nimi.localizedCaseInsensitiveContains(hakuteksti)
let vastaaSuodatinta = !naytaVainKeskeneraiset || !projekti.onValmis
return vastaaHakua && vastaaSuodatinta
}
}
var body: some View {
NavigationStack {
List {
ForEach(suodatetutProjektit) { projekti in
NavigationLink(value: projekti) {
ProjektiRiviView(projekti: projekti)
}
}
.onDelete(perform: poistaProjektit)
}
.searchable(text: $hakuteksti, prompt: "Hae projekteja")
.navigationTitle("Projektit")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Toggle("Keskeneräiset", isOn: $naytaVainKeskeneraiset)
.toggleStyle(.switch)
}
ToolbarItem(placement: .topBarTrailing) {
Button {
lisaaUusiProjekti()
} label: {
Image(systemName: "plus")
}
}
}
.navigationDestination(for: Projekti.self) { projekti in
ProjektiYksityiskohtaView(projekti: projekti)
}
}
}
private func lisaaUusiProjekti() {
let uusi = Projekti(
nimi: "Uusi projekti",
kuvaus: "Kirjoita kuvaus tähän"
)
modelContext.insert(uusi)
}
private func poistaProjektit(at offsets: IndexSet) {
for index in offsets {
modelContext.delete(suodatetutProjektit[index])
}
}
}
// Projektirivin näkymä
struct ProjektiRiviView: View {
let projekti: Projekti
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(projekti.nimi)
.font(.headline)
Spacer()
if projekti.onValmis {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
}
}
Text(projekti.kuvaus)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
HStack {
Text("\(projekti.tehtavat.count) tehtävää")
.font(.caption)
.foregroundColor(.secondary)
if !projekti.tehtavat.isEmpty {
Text("• \(Int(projekti.edistymisprosentti))% valmis")
.font(.caption)
.foregroundColor(.blue)
}
}
}
.padding(.vertical, 4)
}
}
Yhteenveto ja seuraavat askeleet
SwiftData on kehittynyt valtavasti ensimmäisestä julkaisustaan. iOS 26:n myötä se on vihdoin saavuttanut kypsyystason, joka tekee siitä luotettavan valinnan useimpiin iOS-sovelluksiin. Käydään vielä läpi tärkeimmät opit:
- @Model, ModelContainer ja ModelContext muodostavat SwiftDatan ytimen — ymmärrä näiden roolit ja yhteispeli.
- @Query ja FetchDescriptor tarjoavat joustavat kyselymekanismit niin SwiftUI-näkymissä kuin ohjelmallisessakin käytössä.
- Suhteet ja poistosäännöt mahdollistavat monimutkaisten tietomallien rakentamisen — mutta muista cascade-poiston rajoitukset.
- Malliperinnöllisyys (iOS 26) avaa uusia mahdollisuuksia tietomallien suunnitteluun, ja se on vihdoin täällä.
- CloudKit-synkronointi toimii lähes automaattisesti, mutta tietomallien on noudatettava CloudKitin rajoituksia.
- Migraatiostrategiat ovat välttämättömiä sovelluksen elinkaaren hallinnassa — älä jätä niitä viime tippaan.
- @ModelActor mahdollistaa raskaat operaatiot pääsäikeen ulkopuolella turvallisesti.
SwiftData ei vielä kata aivan kaikkia käyttötapauksia — esimerkiksi edistyneet CloudKit-synkronointistrategiat ja julkiset tietokannat vaativat edelleen Core Dataa tai suoraa CloudKit-rajapintaa. Mutta suurimmalle osalle sovelluksia SwiftData on nyt se oikea valinta.
Seuraavaksi suosittelen kokeilemaan näitä konsepteja omassa projektissasi. Aloita yksinkertaisesta mallista, lisää suhteita, kokeile kyselyitä ja rakenna vähitellen monimutkaisempaa tietokantalogiikkaa. Parhaiten oppii tekemällä — ja SwiftDatan kanssa tekeminen on oikeasti yllättävän mukavaa.