Introduktion: SwiftData mognar äntligen
När Apple introducerade SwiftData under WWDC23 var löftet stort: ett modernt, rent Swift-baserat ramverk för datapersistens som skulle ersätta Core Data:s ålderstigna Objective-C-arv med elegant makro-driven syntax. Verkligheten? Ja, den blev en annan. SwiftData i iOS 17 och 18 var plågat av buggar, saknade grundläggande funktioner och fick många erfarna utvecklare att sucka tungt och stanna kvar vid Core Data.
Men med iOS 26 och Xcode 26, lanserade under WWDC25, förändras bilden dramatiskt.
SwiftData har äntligen fått den enskilt mest efterfrågade funktionen — modellarv (class inheritance) — och dessutom en rad kritiska buggfixar som gör ramverket redo för produktionsbruk på riktigt. I den här guiden går vi igenom allt du behöver veta: från grunderna i modellarv och schemamigrering till de subtila förbättringarna i predikat och Codable-stöd som gör vardagen enklare för oss utvecklare.
Har du hållit dig borta från SwiftData hittills? Då kan det vara dags att ompröva. Låt oss dyka in.
Bakgrund: Varför SwiftData behövde förbättras
För att förstå vad iOS 26-uppdateringarna egentligen innebär behöver vi titta tillbaka på problemen som plågade SwiftData sedan lanseringen.
Buggarna som höll utvecklare borta
Det kanske mest frustrerande problemet var att vyer inte uppdaterades korrekt när data muterades via @ModelActor. Om du hade en bakgrundsprocess som uppdaterade modellobjekt — till exempel en synkroniseringstjänst — kunde SwiftUI-vyerna bli helt ur synk med den faktiska datan. Utvecklare var tvungna att implementera manuella workarounds med NotificationCenter eller andra hack för att tvinga fram vyuppdateringar. Inte direkt elegant.
Ett annat stort irritationsmoment var att Codable-egenskaper inte kunde användas i predikat. Om du hade en adresstyp som en Codable-struct inbäddad i din modell gick det helt enkelt inte att filtrera på dess egenskaper — trots att SwiftData internt lagrade varje fält separat i databasen. Det tvingade utvecklare att antingen platta ut sina datamodeller eller använda efterfiltrering i minnet, vilket var både opraktiskt och prestandamässigt tveksamt.
Avsaknaden av modellarv
Core Data har stött entity inheritance sedan allra första versionen. Möjligheten att definiera en basentitet med gemensamma egenskaper och sedan skapa specialiserade underentiteter är helt central för många datamodeller. Tänk dig ett system med olika typer av meddelanden — textmeddelanden, bildmeddelanden, videomeddelanden — som alla delar avsändare, tidsstämpel och lässtatus men har unika egenskaper.
I SwiftData före iOS 26 var detta omöjligt. Punkt. Försök att skapa ärvande @Model-klasser resulterade i kryptiska felmeddelanden eller rena krascher. Utvecklare tvingades istället använda oeleganta lösningar som enumbaserade typmarkeringar eller separata modeller med manuella relationer.
Modellarv i SwiftData: Grunderna
iOS 26 introducerar stöd för klassarv i SwiftData-modeller. Konceptet är bekant för alla som arbetat med objektorienterad programmering eller Core Data: du definierar en basklass med delade egenskaper och skapar underklasser som ärver dessa och lägger till sina egna.
Definiera en basklass
Okej, låt oss börja med ett praktiskt exempel. Vi bygger en app för att hantera resor — precis som i Apples WWDC25-demonstration. Basklassen Trip innehåller egenskaper som alla typer av resor delar:
import SwiftData
@Model
class Trip {
var destination: String
var startDate: Date
var endDate: Date
var notes: String?
init(destination: String, startDate: Date, endDate: Date, notes: String? = nil) {
self.destination = destination
self.startDate = startDate
self.endDate = endDate
self.notes = notes
}
}
Det ser ut precis som en vanlig SwiftData-modell. Magin händer när vi skapar underklasser.
Skapa underklasser
Nu skapar vi två specialiserade restyper: en för affärsresor och en för privata resor. Notera att underklasserna behöver markeras med @available för iOS 26 eller senare, eftersom arvsstödet kräver den nya versionen:
@available(iOS 26, *)
@Model
class BusinessTrip: Trip {
var perDiem: Decimal
var companyName: String
var expenseReportId: String?
init(destination: String, startDate: Date, endDate: Date,
perDiem: Decimal, companyName: String,
notes: String? = nil, expenseReportId: String? = nil) {
self.perDiem = perDiem
self.companyName = companyName
self.expenseReportId = expenseReportId
super.init(destination: destination, startDate: startDate,
endDate: endDate, notes: notes)
}
}
@available(iOS 26, *)
@Model
class PersonalTrip: Trip {
enum Reason: String, Codable {
case vacation
case family
case adventure
case wellness
}
var reason: Reason
var companions: [String]
init(destination: String, startDate: Date, endDate: Date,
reason: Reason, companions: [String] = [],
notes: String? = nil) {
self.reason = reason
self.companions = companions
super.init(destination: destination, startDate: startDate,
endDate: endDate, notes: notes)
}
}
Några viktiga saker att ha koll på:
- Underklasserna ärver alla egenskaper från
Trip— destination, datum och anteckningar finns tillgängliga automatiskt. super.init()måste anropas i underklassens initialiserare för att sätta basklassens egenskaper.@available(iOS 26, *)krävs på varje underklass. Glöm inte det — annars får du kryptiska kompilatorfel.- Varje underklass kan ha sina egna unika egenskaper —
BusinessTriphar traktamente och företagsnamn, medanPersonalTriphar reseanledning och resesällskap.
Registrera underklasser i ModelContainer
En avgörande detalj som det är lätt att missa: du måste explicit registrera alla underklasser i din ModelContainer. Det räcker inte att bara registrera basklassen:
@main
struct TripApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [
Trip.self,
BusinessTrip.self,
PersonalTrip.self
])
}
}
Om du glömmer att inkludera underklasserna kommer SwiftData inte att känna igen dem vid hämtning, och du får oväntade resultat (eller i värsta fall krascher). Jag har själv gjort det misstaget — det kostar en stunds huvudbry.
Frågor och filtrering med arv
En av de mest kraftfulla aspekterna av modellarv i SwiftData är hur det påverkar frågor. Beroende på vilken typ du frågar efter får du olika resultatmängder — och det är faktiskt riktigt snyggt.
Polymorfiska frågor
Om du frågar efter basklassen Trip får du tillbaka alla resor, inklusive affärsresor och privata resor:
// Hämtar ALLA resor - både Trip, BusinessTrip och PersonalTrip
@Query(sort: \Trip.startDate)
var allTrips: [Trip]
Det här är polymorfism i aktion. Varje objekt i arrayen behåller sin faktiska typ, så du kan typcasta dem vid behov:
struct TripListView: View {
@Query(sort: \Trip.startDate) var trips: [Trip]
var body: some View {
List(trips) { trip in
VStack(alignment: .leading) {
Text(trip.destination)
.font(.headline)
if let businessTrip = trip as? BusinessTrip {
Label(businessTrip.companyName, systemImage: "building.2")
.font(.caption)
.foregroundStyle(.blue)
} else if let personalTrip = trip as? PersonalTrip {
Label(personalTrip.reason.rawValue.capitalized,
systemImage: "heart")
.font(.caption)
.foregroundStyle(.pink)
}
Text("\(trip.startDate.formatted(.dateTime.month().day())) – \(trip.endDate.formatted(.dateTime.month().day()))")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
Typspecifika frågor
Om du bara vill hämta en specifik typ frågar du efter den underklassen direkt:
// Hämtar BARA affärsresor
@Query(sort: \BusinessTrip.startDate)
var businessTrips: [BusinessTrip]
// Hämtar BARA privata resor
@Query(filter: #Predicate { trip in
trip.reason == .vacation
})
var vacationTrips: [PersonalTrip]
Den här frågemodellen gör det smidigt att bygga vyer som antingen visar allt eller filtrerar efter specifika typer. Tänk dig en flikbaserad vy med "Alla", "Affärsresor" och "Privata resor" — varje flik använder helt enkelt en fråga med rätt typ. Enkelt och elegant.
Prestandaöverväganden vid frågor
Det är värt att nämna att polymorfiska frågor mot basklassen kan vara något långsammare än typspecifika frågor, eftersom SwiftData internt behöver avgöra den faktiska typen för varje rad. I praktiken är skillnaden försumbar för de flesta appar, men har du tusentals objekt och vet att du bara behöver en specifik typ? Då är det bättre att fråga mot den typen direkt.
Schemamigrering: Från platt modell till arv
Det är en sak att bygga en ny app med modellarv från start. En helt annan — och ärligt talat betydligt vanligare — situation är att migrera en befintlig app. Kanske har du redan en Trip-modell med alla egenskaper i en enda klass och vill nu dela upp den i underklasser. SwiftData:s migreringsverktyg hanterar detta, men det kräver lite planering.
Steg 1: Definiera schemaversioner
Först skapar vi versionerade scheman som representerar vår datamodells utveckling. Varje version wrappas i en enum som uppfyller VersionedSchema:
// Den ursprungliga platta modellen
enum TripSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Trip.self]
}
@Model
class Trip {
var destination: String
var startDate: Date
var endDate: Date
var notes: String?
// Alla egenskaper i en enda klass
var tripType: String? // "business" eller "personal"
var perDiem: Decimal?
var companyName: String?
var reason: String?
init(destination: String, startDate: Date, endDate: Date) {
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
}
}
// Ny version med arvshierarki
@available(iOS 26, *)
enum TripSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Trip.self, BusinessTrip.self, PersonalTrip.self]
}
@Model
class Trip {
var destination: String
var startDate: Date
var endDate: Date
var notes: String?
init(destination: String, startDate: Date, endDate: Date,
notes: String? = nil) {
self.destination = destination
self.startDate = startDate
self.endDate = endDate
self.notes = notes
}
}
@Model
class BusinessTrip: Trip {
var perDiem: Decimal
var companyName: String
init(destination: String, startDate: Date, endDate: Date,
perDiem: Decimal, companyName: String, notes: String? = nil) {
self.perDiem = perDiem
self.companyName = companyName
super.init(destination: destination, startDate: startDate,
endDate: endDate, notes: notes)
}
}
@Model
class PersonalTrip: Trip {
var reason: String
init(destination: String, startDate: Date, endDate: Date,
reason: String, notes: String? = nil) {
self.reason = reason
super.init(destination: destination, startDate: startDate,
endDate: endDate, notes: notes)
}
}
}
Steg 2: Skapa en migreringsplan
Migreringen från en platt modell till en arvshierarki kräver en anpassad migrering (custom migration). Lättviktsmigrering räcker inte här, eftersom SwiftData behöver konvertera befintliga rader till rätt underklasstyp:
@available(iOS 26, *)
enum TripMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[TripSchemaV1.self, TripSchemaV2.self]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: TripSchemaV1.self,
toVersion: TripSchemaV2.self,
willMigrate: { context in
// Hämta alla befintliga resor
let descriptor = FetchDescriptor()
let existingTrips = try context.fetch(descriptor)
for trip in existingTrips {
// Konvertera baserat på tripType-fältet
switch trip.tripType {
case "business":
let businessTrip = TripSchemaV2.BusinessTrip(
destination: trip.destination,
startDate: trip.startDate,
endDate: trip.endDate,
perDiem: trip.perDiem ?? 0,
companyName: trip.companyName ?? "Okänt företag",
notes: trip.notes
)
context.insert(businessTrip)
context.delete(trip)
case "personal":
let personalTrip = TripSchemaV2.PersonalTrip(
destination: trip.destination,
startDate: trip.startDate,
endDate: trip.endDate,
reason: trip.reason ?? "semester",
notes: trip.notes
)
context.insert(personalTrip)
context.delete(trip)
default:
// Behåll som vanlig Trip
break
}
}
try context.save()
},
didMigrate: nil
)
static var stages: [MigrationStage] {
[migrateV1toV2]
}
}
Steg 3: Konfigurera ModelContainer med migreringsplanen
Slutligen kopplar vi ihop alltihop i appens startpunkt:
@available(iOS 26, *)
@main
struct TripApp: App {
let container: ModelContainer
init() {
do {
container = try ModelContainer(
for: TripSchemaV2.Trip.self,
TripSchemaV2.BusinessTrip.self,
TripSchemaV2.PersonalTrip.self,
migrationPlan: TripMigrationPlan.self
)
} catch {
fatalError("Kunde inte initialisera ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(container)
}
}
}
En viktig detalj: willMigrate-blocket körs innan schemat faktiskt ändras, vilket innebär att du fortfarande arbetar med V1-modellerna. Det är därför vi hämtar data med TripSchemaV1.Trip. Behöver du göra justeringar efter att schemat har ändrats? Då använder du didMigrate istället.
Lättviktsmigrering: När det räcker
Inte alla schemaändringar kräver anpassad migrering. SwiftData kan faktiskt hantera en hel del vanliga ändringar automatiskt med lättviktsmigrering. Här är vad SwiftData klarar utan extra kod:
- Lägga till nya egenskaper — så länge de har ett standardvärde eller är optionella
- Ta bort egenskaper — datan raderas, men migreringen lyckas
- Byta namn på egenskaper — med hjälp av
@Attribute(originalName:) - Ändra relationstyper — exempelvis från en-till-en till en-till-många
- Lägga till nya entiteter — inklusive underklasser i iOS 26
Om du lägger till en ny underklass till en befintlig arvshierarki (alltså inte konverterar från platt till arv, utan bygger vidare på en redan existerande hierarki) räcker det med en lättviktsmigrering:
// Lägga till en ny restyp till en befintlig hierarki
@available(iOS 26, *)
enum TripSchemaV3: VersionedSchema {
static var versionIdentifier = Schema.Version(3, 0, 0)
static var models: [any PersistentModel.Type] {
[Trip.self, BusinessTrip.self, PersonalTrip.self, GroupTrip.self]
}
// Befintliga klasser oförändrade...
@Model
class GroupTrip: Trip {
var groupSize: Int
var organizer: String
init(destination: String, startDate: Date, endDate: Date,
groupSize: Int, organizer: String, notes: String? = nil) {
self.groupSize = groupSize
self.organizer = organizer
super.init(destination: destination, startDate: startDate,
endDate: endDate, notes: notes)
}
}
}
// Lättviktsmigrering räcker!
static let migrateV2toV3 = MigrationStage.lightweight(
fromVersion: TripSchemaV2.self,
toVersion: TripSchemaV3.self
)
Codable-egenskaper i predikat: Äntligen!
Ärligt talat — det här är en av de förbättringar jag uppskattar mest. Att Codable-egenskaper nu kan användas i predikat i iOS 26 låter kanske som en liten sak, men det förändrar helt hur vi kan strukturera våra datamodeller.
Hur SwiftData hanterar Codable internt
När du använder en Codable-typ som egenskap i en SwiftData-modell sker något intressant under huven. Istället för att serialisera hela strukturen till en binär blob (som Core Data gör med Value Transformers) skapar SwiftData separata kolumner för varje egenskap i den Codable-typen. Apple kallar detta composite attributes.
struct Address: Codable {
var street: String
var city: String
var postalCode: String
var country: String
}
@Model
class Contact {
var name: String
var email: String
var address: Address
init(name: String, email: String, address: Address) {
self.name = name
self.email = email
self.address = address
}
}
I databasen lagras detta inte som en enda kolumn med JSON-data, utan som separata kolumner: address_street, address_city, address_postalCode och address_country. Det är just den här interna representationen som möjliggör direkt filtrering.
Filtrera på Codable-egenskaper
I iOS 26 kan du äntligen skriva predikat som dessa:
// Filtrera kontakter i en specifik stad
@Query(filter: #Predicate { contact in
contact.address.city == "Stockholm"
})
var stockholmContacts: [Contact]
// Kombinera Codable-egenskaper med andra filter
let predicate = #Predicate { contact in
contact.address.country == "Sverige" &&
contact.name.contains("son")
}
let descriptor = FetchDescriptor(
predicate: predicate,
sortBy: [SortDescriptor(\Contact.address.city)]
)
let results = try context.fetch(descriptor)
Före iOS 26 hade den här koden genererat ett körtidsfel. Nu fungerar det precis som förväntat — och filtreringen sker på databasnivå, inte i minnet. Det är en enorm skillnad, speciellt i appar med stora datamängder.
Varningar och begränsningar
Det finns dock några viktiga saker att tänka på när du använder Codable-typer i SwiftData:
- Undvik att ändra Codable-typens struktur: Att lägga till, ta bort eller byta namn på egenskaper i en Codable-typ kan bryta lättviktsmigreringar. Eftersom varje egenskap motsvarar en databaskolumn behandlas strukturändringar som schemaändringar.
- Arrayer av Codable-typer beter sig annorlunda: Om du har en egenskap som
var addresses: [Address]lagras den som en serialiserad blob, inte som composite attributes. Du kan alltså inte filtrera på enskilda element i arrayen. - Håll Codable-typerna enkla: Håll dig till grundläggande typer som String, Int, Double och Date. Komplexa typer (som wrapper-klasser kring UIColor eller liknande) kan orsaka fatala fel vid schemavalidering.
@ModelActor: Buggfixen som förändrar allt
Den andra stora buggfixen i iOS 26 handlar om @ModelActor — makrot som låter dig utföra SwiftData-operationer på bakgrundstrådar. Problemet var att SwiftUI-vyer helt enkelt inte uppdaterades när data modifierades av en ModelActor. Det här var ett riktigt allvarligt problem som i praktiken gjorde bakgrundsbearbetning oanvändbar i kombination med SwiftUI.
Problemet före iOS 26
Tänk dig det här scenariot: du har en synkroniseringsprocess som hämtar data från ett API och sparar den via en ModelActor:
@ModelActor
actor SyncService {
func syncTrips() async throws {
let apiTrips = try await APIClient.fetchTrips()
for apiTrip in apiTrips {
let trip = Trip(
destination: apiTrip.destination,
startDate: apiTrip.startDate,
endDate: apiTrip.endDate
)
modelContext.insert(trip)
}
try modelContext.save()
}
}
I iOS 17 och 18 kunde du anropa den här funktionen och se att datan sparades korrekt i databasen — men SwiftUI-vyer som använde @Query uppdaterades helt enkelt inte. Användaren behövde stänga och öppna appen för att se de nya resorna. Fruktansvärt.
Lösningen i iOS 26
I iOS 26 har Apple fixat den underliggande notifieringsmekanismen så att ändringar via @ModelActor korrekt propageras till SwiftUI:s observationssystem. @Query-wrappern fångar nu upp dessa ändringar och utlöser omritning av vyn automatiskt.
Och det bästa? Denna fix är bakåtkompatibel med iOS 17. När du kompilerar med Xcode 26 får du det korrekta beteendet även när appen körs på äldre iOS-versioner. Du behöver alltså inte bumpa din minimum deployment target för att dra nytta av förbättringen. Det tycker jag är riktigt snyggt av Apple.
Bästa praxis för @ModelActor
Med buggfixen på plats kan vi äntligen använda @ModelActor som det var tänkt. Här är ett mer komplett exempel som visar ett rekommenderat mönster:
@ModelActor
actor TripRepository {
// Hämta och filtrera resor i bakgrunden
func fetchUpcomingTrips() throws -> [Trip] {
let now = Date()
let predicate = #Predicate { trip in
trip.startDate > now
}
let descriptor = FetchDescriptor(
predicate: predicate,
sortBy: [SortDescriptor(\Trip.startDate)]
)
return try modelContext.fetch(descriptor)
}
// Skapa nya resor i bakgrunden
func createTrip(destination: String, start: Date, end: Date) throws {
let trip = Trip(
destination: destination,
startDate: start,
endDate: end
)
modelContext.insert(trip)
try modelContext.save()
}
// Batchuppdatering
func markAllAsReviewed(_ tripIds: [PersistentIdentifier]) throws {
for id in tripIds {
if let trip = modelContext.model(for: id) as? Trip {
trip.notes = (trip.notes ?? "") + "\n[Granskad]"
}
}
try modelContext.save()
}
}
Det centrala mönstret här är att isolera databasoperationer i en dedikerad aktör och använda PersistentIdentifier för att referera till objekt över aktörsgränser. Skicka aldrig @Model-objekt mellan aktörer — de är inte Sendable.
Praktiskt exempel: En komplett resehanteringsapp
Dags att knyta ihop säcken. Låt oss bygga kärndelarna av en resehanteringsapp som använder modellarv, Codable-egenskaper och bakgrundssynkronisering.
Datamodellen
import SwiftData
import Foundation
// Codable-typ för platsdetaljer
struct Location: Codable {
var city: String
var country: String
var latitude: Double?
var longitude: Double?
}
// Basklass
@Model
class Trip {
var title: String
var location: Location
var startDate: Date
var endDate: Date
var notes: String?
var isFavorite: Bool
var duration: Int {
Calendar.current.dateComponents([.day], from: startDate, to: endDate).day ?? 0
}
init(title: String, location: Location, startDate: Date,
endDate: Date, notes: String? = nil, isFavorite: Bool = false) {
self.title = title
self.location = location
self.startDate = startDate
self.endDate = endDate
self.notes = notes
self.isFavorite = isFavorite
}
}
// Affärsresa
@available(iOS 26, *)
@Model
class BusinessTrip: Trip {
var perDiem: Decimal
var companyName: String
var meetingAgenda: String?
var totalAllowance: Decimal {
let days = Decimal(duration)
return perDiem * days
}
init(title: String, location: Location, startDate: Date,
endDate: Date, perDiem: Decimal, companyName: String,
meetingAgenda: String? = nil) {
self.perDiem = perDiem
self.companyName = companyName
self.meetingAgenda = meetingAgenda
super.init(title: title, location: location,
startDate: startDate, endDate: endDate)
}
}
// Semesterresa
@available(iOS 26, *)
@Model
class VacationTrip: Trip {
var budget: Decimal
var activities: [String]
var accommodationType: String
init(title: String, location: Location, startDate: Date,
endDate: Date, budget: Decimal, activities: [String] = [],
accommodationType: String = "Hotell") {
self.budget = budget
self.activities = activities
self.accommodationType = accommodationType
super.init(title: title, location: location,
startDate: startDate, endDate: endDate)
}
}
SwiftUI-vyer med arvsstöd
import SwiftUI
import SwiftData
struct TripDashboard: View {
@Query(sort: \Trip.startDate, order: .reverse)
var allTrips: [Trip]
@State private var selectedFilter: TripFilter = .all
enum TripFilter: String, CaseIterable {
case all = "Alla"
case business = "Affärsresor"
case vacation = "Semesterresor"
}
var filteredTrips: [Trip] {
switch selectedFilter {
case .all:
return allTrips
case .business:
return allTrips.compactMap { $0 as? BusinessTrip }
case .vacation:
return allTrips.compactMap { $0 as? VacationTrip }
}
}
var body: some View {
NavigationStack {
VStack {
Picker("Filter", selection: $selectedFilter) {
ForEach(TripFilter.allCases, id: \.self) { filter in
Text(filter.rawValue).tag(filter)
}
}
.pickerStyle(.segmented)
.padding(.horizontal)
List(filteredTrips) { trip in
TripRow(trip: trip)
}
}
.navigationTitle("Mina resor")
}
}
}
struct TripRow: View {
let trip: Trip
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(trip.title)
.font(.headline)
Text("\(trip.location.city), \(trip.location.country)")
.font(.subheadline)
.foregroundStyle(.secondary)
// Typspecifik information
Group {
if let business = trip as? BusinessTrip {
Label(business.companyName, systemImage: "briefcase")
.foregroundStyle(.blue)
} else if let vacation = trip as? VacationTrip {
Label(vacation.accommodationType,
systemImage: "bed.double")
.foregroundStyle(.green)
}
}
.font(.caption)
}
Spacer()
VStack(alignment: .trailing) {
Text("\(trip.duration) dagar")
.font(.caption)
.foregroundStyle(.secondary)
if trip.isFavorite {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}
}
}
.padding(.vertical, 4)
}
}
Filtrering med Codable-egenskaper
struct TripsInCountryView: View {
let country: String
// Filtrera direkt på Codable-egenskaper - fungerar i iOS 26!
@Query var trips: [Trip]
init(country: String) {
self.country = country
_trips = Query(
filter: #Predicate { trip in
trip.location.country == country
},
sort: \Trip.startDate
)
}
var body: some View {
List(trips) { trip in
TripRow(trip: trip)
}
.navigationTitle("Resor i \(country)")
}
}
Vanliga misstag och hur du undviker dem
Efter att ha grävt ner mig i SwiftData:s nya arvsstöd finns det ett antal fallgropar som jag vill lyfta fram. Spara dig själv lite huvudvärk och läs igenom dessa.
1. Glömma att registrera underklasser
Det vanligaste misstaget. Att bara registrera basklassen i ModelContainer och sedan undra varför ingenting fungerar. Alla underklasser måste explicit inkluderas, annars ignoreras de vid frågor och insättningar.
2. Modifiera Codable-strukturer utan migreringsplan
Eftersom SwiftData skapar separata kolumner för varje egenskap i en Codable-typ, behandlas strukturella ändringar som schemaändringar. Att lägga till ett fält i din Address-struct utan att uppdatera din VersionedSchema kan leda till krascher:
// DÅLIGT: Ändra Codable-struct utan migreringsplan
struct Address: Codable {
var street: String
var city: String
var postalCode: String
var country: String
var apartment: String? // Nytt fält - kräver migrering!
}
Lösningen: behandla alltid ändringar i Codable-typer som schemaändringar och skapa en ny schemaversion.
3. Skicka @Model-objekt mellan aktörer
@Model-klasser är inte Sendable och ska aldrig skickas direkt mellan aktörer. Använd PersistentIdentifier istället:
// DÅLIGT: Skicka modellobjekt
let trip = try await repository.fetchTrip() // Kompilatorfel!
// BRA: Skicka identifier
let tripId = trip.persistentModelID
let updatedTrip = try await repository.updateTrip(id: tripId)
4. Använda arv för allting
Bara för att modellarv nu finns tillgängligt betyder det inte att det är rätt lösning för varje situation. Arv fungerar bäst när modellerna bildar en naturlig hierarki med delade egenskaper. Om dina "underklasser" knappt delar något med basklassen? Då är det ofta bättre med separata modeller och relationer.
5. Blanda @available-nivåer
Om din app stöder äldre iOS-versioner behöver du hantera fallet där underklasserna inte är tillgängliga. Se till att wrappa villkorlig logik kring arvshierarkin:
if #available(iOS 26, *) {
// Använd underklasser
let businessTrips = try context.fetch(
FetchDescriptor()
)
} else {
// Fallback till platt modell
let trips = try context.fetch(
FetchDescriptor()
).filter { $0.tripType == "business" }
}
SwiftData kontra Core Data: Ska du migrera nu?
Med iOS 26-förbättringarna uppstår den naturliga frågan: är det dags att migrera från Core Data till SwiftData?
Det korta svaret: det beror på. Men bilden är klart ljusare nu än för ett år sedan.
Argument för att migrera
- Renare syntax: SwiftData:s makrobaserade approach är radikalt enklare än Core Data:s konfigurationstunga setup med .xcdatamodeld-filer, NSManagedObject-subklasser och fetch requests.
- SwiftUI-integration:
@Queryoch@Modelfungerar sömlöst med SwiftUI:s observationssystem — särskilt nu när@ModelActor-buggen är fixad. - Modellarv: Med iOS 26 har SwiftData nu paritet med Core Data på denna front.
- Framtiden: Core Data fick inga nya funktioner under WWDC25 — för andra året i rad. Det är ganska tydligt var Apple lägger sin energi.
Argument för att vänta
- Mognad: SwiftData är fortfarande relativt ungt. Trots buggfixarna kan det finnas edge cases som ännu inte dykt upp i produktion.
- Funktionsgap: SwiftData saknar fortfarande vissa funktioner som Core Data erbjuder, exempelvis publika/delade CloudKit-databaser och mer avancerade predikatmöjligheter.
- Befintlig kodbas: Om du har en stor, stabil Core Data-kodbas med komplexa migreringar kan migreringskostnaden vara betydande. Ibland är "if it ain't broke, don't fix it" faktiskt bästa strategin.
Min pragmatiska rekommendation: använd SwiftData för nya projekt och överväg migration för befintliga projekt när du ändå gör en större omskrivning. Tvinga inte in en migrering bara för migrationens skull.
Sammanfattning och framåtblick
iOS 26 markerar en verklig vändpunkt för SwiftData. Med modellarv, fixad @ModelActor-observering och fungerande Codable-predikat har ramverket äntligen kommit till den punkt där det genuint kan vara förstahandsvalet för datapersistens i de flesta iOS-appar.
Låt oss sammanfatta de viktigaste nyheterna:
- Modellarv låter dig bygga naturliga arvshierarkier med
@Model-klasser, med stöd för polymorfiska frågor och typspecifik filtrering. - Schemamigrering med
VersionedSchemaochSchemaMigrationPlanhanterar övergången till arvshierarkier på ett strukturerat sätt. - Codable-predikat fungerar äntligen, vilket möjliggör effektiv databasfiltrering på inbäddade strukturer.
- @ModelActor-observering är fixad — och bakåtkompatibel ända ner till iOS 17.
För oss SwiftUI-utvecklare innebär det här att vi kan bygga appar med rikare datamodeller, bättre separation mellan UI och datalager, och pålitlig bakgrundssynkronisering — allt med ren, idiomatisk Swift-kod. SwiftData har mognat. Det är dags att ta det på allvar.