UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Help! Please help, I am stuck: SwiftData Migration

Forums > Swift

Hi,

I've tried so hard and read a lot - but I do not find the mistake. I have no glue.

It ist just one step more than in Paul's book with a complex migration. As in his example V1 to V2 runs wunderful. Then I would like to make a next migration and add only one String property to my class user. The app crashes in run-time as soon as a new object (a new user) is asked to be created. The error message is: SwiftData/BackingData.swift:432: Fatal error: Expected only Arrays for Relationships - String

But there are no relationships!

Any hint is welcomed, even just a link to a "source of wisdom". Without solving this problem I cannot proceed in developing my real app. Makes no sense without the capapbility of a migration.

Many thanks in advance. Ralf

Here is my code:

import Foundation
import SwiftData

enum UserSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1,0,0)

    static var models: [any PersistentModel.Type] {
        [User.self]
    }

    @Model
    class User {
        var name: String
        var age: Int

        init(name: String = "", age: Int = 0) {
            self.name = name
            self.age = age
        }
    }
}

enum UserSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2,0,0)

    static var models: [any PersistentModel.Type] {
        [User.self]
    }

    @Model
    class User {
        @Attribute(.unique) var name: String
        var age: Int

        init(name: String = "", age: Int = 0) {
            self.name = name
            self.age = age
        }
    }
}

enum UserSchemaV3: VersionedSchema {
    static var versionIdentifier = Schema.Version(3,0,0)

    static var models: [any PersistentModel.Type] {
        [User.self]
    }

    @Model
    class User {
        var name: String
        var age: Int
        var gender: String

        init(name: String = "", age: Int = 0, gender: String = "") {
            self.name = name
            self.age = age
            self.gender = gender
        }
    }
}

typealias User = UserSchemaV3.User

enum UsersMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [UserSchemaV1.self, UserSchemaV2.self, UserSchemaV3.self]
    }

    static let migrateV1toV2 = MigrationStage.custom (
        fromVersion: UserSchemaV1.self,
        toVersion: UserSchemaV2.self,
    willMigrate: {context in
//         remove duplicates
        let users = try context.fetch(FetchDescriptor<UserSchemaV1.User>())
        var usedNames = Set<String>()

        for user in users {
            if usedNames.contains(user.name) {
                context.delete(user)
            }
            usedNames.insert(user.name)
        }

        try context.save()
    }, didMigrate: nil
    )

    static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: UserSchemaV2.self,
        toVersion: UserSchemaV3.self,
        willMigrate: { context in
            // Füge das 'gender' Feld mit einem Standardwert hinzu
            let users = try context.fetch(FetchDescriptor<UserSchemaV2.User>())

            for user in users {
                // Konvertiere zu UserSchemaV3.User und setze 'gender' auf einen Standardwert
                let newUser = UserSchemaV3.User(name: user.name, age: user.age, gender: "")
                context.insert(newUser)
                context.delete(user)
            }

            try context.save()
        }, didMigrate: nil
    )

    static var stages: [MigrationStage] {
        [migrateV1toV2,migrateV2toV3]
    }
}

2      

Hi, if the first part works all fine, so I will address the issue in the second part. As you only add property for new model, you can just make lightweight migration. For lightweight migration swift will automatically migrate your users' data in case of adding new properties with default values.

so just try to add this instead of your custom migration V2ToV3

  static let migrateV2toV3 = MigrationStage.lightweight(
        fromVersion: UserSchemaV2.self,
        toVersion: UserSchemaV3.self
    )

3      

Hi there,

Thank you very much for your help. I didn't discover the case lightweight in the documentation. Thanks for opening my eyes.

It goes a little step further but stops with the next error message:

CoreData: error: Store failed to load. <NSPersistentStoreDescription: 0x600000c666d0> (type: SQLite, url: file:///Users/ralf2/Library/Developer/CoreSimulator/Devices/A98CA6CF-048D-4F3F-8FA2-DBD1BF4A1228/data/Containers/Data/Application/37EFED85-A1A3-476A-A6F2-DB37FC67618A/Library/Application%20Support/default.store) with error = Error Domain=NSCocoaErrorDomain Code=134504 "Cannot use staged migration with an unknown model version." UserInfo={NSLocalizedDescription=Cannot use staged migration with an unknown model version.} with userInfo { NSLocalizedDescription = "Cannot use staged migration with an unknown model version."; }

I cannot see why the model version of UserSchemaV3 is not declared correctly. Do you have a next eye opener?

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Not 100% sure but there is a discrepancy between you V2 and V3. In V2 your have @Attribute(.unique) var name: String and in V3 you again have it as var name: String. Make it both with attribute and check if this solve the issue.

2      

Thank you for your reply and pointing to a mistake in the declaration of the property.

Unfortunately the error meesage is still the same

2      

Did you try to delete app on simulator and then run it again? Or clean build folder?

The same logic works in my case. Post the full setup below:

import SwiftUI
import SwiftData

@main
struct VersioningTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: User.self)
    }

    @MainActor
    var container: ModelContainer {
        do {
            let container = try ModelContainer(
                for: User.self,
                migrationPlan: UserMigrationPlan.self
            )
            return container
        } catch {
            fatalError("Could not create ModelContainer with migration plan: \(error.localizedDescription)")
        }
    }
}
import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var users: [User]

    var body: some View {
        NavigationStack {
            List(users) { user in
                Text("Username: \(user.name). Age: \(user.age). Gender: \(user.gender)")
            }
            .navigationTitle("Versioning")
            .toolbar {
                ToolbarItem {
                    ControlGroup {
                        Button("V1", systemImage: "1.square") {
                            User.addVersion1Data(modelContext: modelContext)
                        }

                        Button("V2", systemImage: "2.square") {
                            User.addVersion2Data(modelContext: modelContext)
                        }

                        Button("V3", systemImage: "3.square") {
                            User.addVersion3Data(modelContext: modelContext)
                        }
                    }
                    .controlGroupStyle(.navigation)
                }
            }
        }
    }
}

#Preview {
    ContentView()
        .modelContainer(User.preview)
}
import Foundation
import SwiftData

typealias User = UserSchemaV3.User

enum UserSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)

    static var models: [any PersistentModel.Type] { [User.self] }

    @Model
    class User {
        var name: String
        var age: Int

        init(
            name: String,
            age: Int
        ) {
            self.name = name
            self.age = age
        }
    }
}

enum UserSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)

    static var models: [any PersistentModel.Type] { [User.self] }

    @Model
    class User {
        @Attribute(.unique) var name: String
        var age: Int

        init(
            name: String,
            age: Int
        ) {
            self.name = name
            self.age = age
        }
    }
}

enum UserSchemaV3: VersionedSchema {
    static var versionIdentifier = Schema.Version(3, 0, 0)

    static var models: [any PersistentModel.Type] { [User.self] }

    @Model
    class User {
        @Attribute(.unique) var name: String
        var age: Int
        var gender: String

        init(
            name: String,
            age: Int,
            gender: String = "N/A"
        ) {
            self.name = name
            self.age = age
            self.gender = gender
        }
    }
}

enum UserMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [UserSchemaV1.self, UserSchemaV2.self, UserSchemaV3.self]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: UserSchemaV1.self,
        toVersion: UserSchemaV2.self,
        willMigrate: {
            context in
            let users = try context.fetch(FetchDescriptor<UserSchemaV1.User>())
            var usedNames = Set<String>()

            for user in users {
                if usedNames.contains(user.name) {
                    context.delete(user)
                }
                usedNames.insert(user.name)
            }

            try context.save()
        },
        didMigrate: nil
    )

    static let migrateV2toV3 = MigrationStage.lightweight(
        fromVersion: UserSchemaV2.self,
        toVersion: UserSchemaV3.self
    )

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }
}

// MARK: - MOCK DATA
extension User {
    @MainActor
    static var preview: ModelContainer {
        let container = try! ModelContainer(for: User.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
        return container
    }

    // MOCK DATA FOR V1
    static func addVersion1Data(modelContext: ModelContext) {
        let users: [User] = [
            User(name: "Alice", age: 25),
            User(name: "Bob", age: 30),
            User(name: "Charlie", age: 28),
            User(name: "David", age: 35),
            User(name: "Eve", age: 27),
            User(name: "Frank", age: 32),
            User(name: "Grace", age: 29),
            User(name: "Henry", age: 31),
            User(name: "Ivy", age: 26),
            User(name: "Jack", age: 33)
        ]

        for user in users {
            modelContext.insert(user)
        }
    }

    // MOCK DATA FOR V2
    static func addVersion2Data(modelContext: ModelContext) {
        let users: [User] = [
            User(name: "Alice", age: 25),
            User(name: "Bob", age: 30),
            User(name: "Charlie", age: 28),
            User(name: "David", age: 35),
            User(name: "Eve", age: 27),
            User(name: "Frank", age: 32),
            User(name: "Grace", age: 29),
            User(name: "Henry", age: 31),
            User(name: "Ivy", age: 26),
            User(name: "Jack", age: 33),
            User(name: "Frank", age: 32),
            User(name: "Grace", age: 29),
            User(name: "Henry", age: 31),
            User(name: "Ivy", age: 26),
            User(name: "Jack", age: 33),
            User(name: "Yuri", age: 42)
        ]

        for user in users {
            modelContext.insert(user)
        }
    }

    // MOCK DATA FOR V3
    static func addVersion3Data(modelContext: ModelContext) {
        let users: [User] = [
            User(name: "Alice", age: 25),
            User(name: "Bob", age: 30),
            User(name: "Charlie", age: 28),
            User(name: "David", age: 35),
            User(name: "Eve", age: 27),
            User(name: "Frank", age: 32),
            User(name: "Grace", age: 29),
            User(name: "Henry", age: 31),
            User(name: "Ivy", age: 26),
            User(name: "Jack", age: 33),
            User(name: "Frank", age: 32),
            User(name: "Grace", age: 29),
            User(name: "Henry", age: 31),
            User(name: "Ivy", age: 26),
            User(name: "Jack", age: 33),
            User(name: "Yuri", age: 42),
            User(name: "Sam", age: 33, gender: "Male"),
            User(name: "Renat", age: 42, gender: "Male"),
            User(name: "Natali", age: 23, gender: "Female")

        ]

        for user in users {
            modelContext.insert(user)
        }
    }
}

2      

Wow! Thank you very much for your support.

To clean the build folder or deleting the app didn't help with my code.

When setting up a new app with your code it runs perfectly.

On a very rough analysis I see the main difference in the top level file, mine is much more simpler

//
//  TestMigrationApp.swift
//  TestMigration
//
//  Created by Ralf 2 on 13.01.24.
//

import SwiftUI
import SwiftData

@main
struct TestMigrationApp: App {

    let container: ModelContainer
//    let myContext: ModelContext

    init() {
        do {
            container = try ModelContainer(
                for: User.self,
                migrationPlan: UsersMigrationPlan.self
            )
        }
        catch {
            fatalError("Failed to initialize model container.")
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

Now I do have to read and understand your approach, especially the functionality of the wrapper @MainActor It seems that I've missed some part of knowledge.

To have the chance to inform you about my progress, I'll leave this "ticket" open. Please, give me a few days for reading, learning and testing. My real app I am working on includes relationships and I have to adapt my learnings from these test to it.

Many thanks again. Wonderful to have such a supportive community for help.

2      

Just small note. I made a mistake there is slight addition should be made. But in any case I run the app and it worked the same way without a hitch.

var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container) // <-- I forgot to use container instead of User.self
    }

As for @MainActor this part is not really important in that particular case. It just makes sure the container is created on the main thread, but as far as I can see it works just fine without it.

2      

Thanks for the reply and the correction.

If the @MainActor is not the "big" difference I have to be more smarter to discover my previous mistake ;-)

I'll rebuild my test app step by step like in a real devlopment process and will see how far or smooth it runs.

Thanks again.

2      

Thank you for sharing

2      

@ygeras

Again many thanks for your ideas and code. If I use your code, everything works fine.

But if I simulate the development, starting with User.Version1, than make the next step to User.Version2 the migration works wonderful. But again, as soon as I do the next migration step to User.Version3, the app crashes.

My method to add the sample data is in both steps, Verion 1 and Version 2, as described below. The only difference is the explicit try? modelContext.save().

I am just wondering why the behaviour is different. But I'll proceed with my real project with your ideas and hope to have more success.

Thanks,

Ralf

    func addSample() {
        let user1 = User(name: "Peter", age: 18)
        let user2 = User(name: "Ralf", age: 59)
        let user3 = User(name: "Michael", age: 57)
        let user4 = User(name: "Reinhold", age: 58)
        let user5 = User(name: "Peter", age: 65)
        let user6 = User(name: "Peter", age: 16)

        modelContext.insert(user1)
        modelContext.insert(user2)
        modelContext.insert(user3)
        modelContext.insert(user4)
        modelContext.insert(user5)
        modelContext.insert(user6)

        try? modelContext.save()
    }

2      

Hi Ralf! But why do you use modelContext.save()? Did you change ModelContainer autosave to off? And what does the crash say? I mean is there any output to the console to indicate the reason?

2      

Hi helpful @ygeras,

to answer your question: I've used the save-method because Paul always recommends to use methods explicit to have control.

But in the meanwhile I've tested my code without it and it males no difference.

Working on my project I was thinking if the lightwight method may be the problem. So I did first another kind of lightweigted migration, changing a property to an optional property. Runs correctly, even the model is more complex with a one-to-many relationship.

The next step was to add a new property like in the test app we discussed above, but unfortunately again the app crashes runtime.

The error message is: Fatal error: Could not create ModelContainer with migration plan: The operation couldn’t be completed. (SwiftData.SwiftDataError error 1.)

or a little bit abouve in yellow/orange: {Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration." UserInfo={entity=Tagebuchseite, attribute=testString, reason=Validation error missing attribute values on mandatory destination attribute}}}

Update: When I define the new property as an optional the property seems to be created but not initialized with the values given in the init() method of the class:

    @Model
    class Tagebuchseite {
        @Attribute(.unique) var datum: Date
        var datumString: String?
        var tagebuchseiteId: UUID
        @Relationship(deleteRule: .cascade, inverse: \Tagebucheintrag.tagebuchseite) var tagebucheintraege: [Tagebucheintrag]?
        var testString: String?

        init(datum: Date = .now, datumString: String = Date.now.formatted(date: .numeric, time: .omitted), tagebuchseiteId: UUID = UUID(), tagebucheintraege: [Tagebucheintrag] = [Tagebucheintrag](), testString: String = "Ein String") {
            self.datum = datum
            self.datumString = datumString
            self.tagebuchseiteId = tagebuchseiteId
            self.tagebucheintraege = tagebucheintraege
            self.testString = testString
        }
    }

1      

Well, without more or less full picture of the code to be migrated, it is rather difficult to say, what might be the problem.

1      

It is no commercial project, so I have no problem to share my code here. It is just a simple form of a diary app to make notes about the weather, nature and activities. Below you will find the main parts of the app that you can get an impression of the models and migration.

Any ideas are welcome.

//
//  Tagebuch_v5App.swift
//  Tagebuch v5
//
//  Created by Ralf 2 on 15.12.23.
//

import SwiftUI
import SwiftData

@main
struct Tagebuch_v5App: App {
    var body: some Scene {

        WindowGroup {
            LaunchView()
        }
        .modelContainer(container)
    }

    @MainActor
    var container: ModelContainer {
        do {
            let container = try ModelContainer(
                for: Tagebuchseite.self,
                migrationPlan: TagebuchseiteMigrationPlan.self
            )
            return container
        } catch {
            fatalError("Could not create ModelContainer with migration plan: \(error.localizedDescription)")
        }
    }
}
//
//  LaunchView.swift
//  StepsHundegesundheit
//
//  Created by Ralf 2 on 31.10.23.
//

import SwiftUI
import SwiftData

struct LaunchView: View {

    @Environment(\.modelContext) var modelContext

//    @Query var tageseintraege : [Tageseintrag]

    @State var tabIndex = 1

    var body: some View {

        TabView(selection: $tabIndex) {
            ContentView()
                .tabItem {
                    VStack {
                        Image(systemName: "house")
                        Text("Einträge")
                    }
                }
                .tag(1)

            DatenSuchView()
                .tabItem {
                    VStack {
                        Image(systemName: "text.magnifyingglass")
                        Text("Suche")
                    }
                }
                .tag(2)
        }
    }
}

#Preview {
    LaunchView()
}
//
//  Tagebuchseite.swift
//  Tagebuch v5
//
//  Created by Ralf 2 on 15.12.23.
//

import Foundation
import SwiftData

typealias Tagebuchseite = TagebuchseiteSchemaV4.Tagebuchseite
typealias Tagebucheintrag = TagebuchseiteSchemaV4.Tagebucheintrag

enum TagebuchseiteSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1,0,0)

    static var models: [any PersistentModel.Type] {[Tagebuchseite.self]}

    @Model
    class Tagebuchseite {
        var datum: Date
        var datumString: String
        var tagebuchseiteId: UUID
        @Relationship(deleteRule: .cascade, inverse: \Tagebucheintrag.tagebuchseite) var tagebucheintraege: [Tagebucheintrag]?

        init(datum: Date = .now, datumString: String = Date.now.formatted(date: .numeric, time: .omitted), tagebuchseiteId: UUID = UUID(), tagebucheintraege: [Tagebucheintrag] = [Tagebucheintrag]()) {
            self.datum = datum
            self.datumString = datumString
            self.tagebuchseiteId = tagebuchseiteId
            self.tagebucheintraege = tagebucheintraege
        }
    }

    @Model
    class Tagebucheintrag {
        var aktivitaet: String?
        var fauna: String?
        var flora: String?
        var himmel: String?
        var ort: String?
        var temperatur: String?
        var uhrzeit: String?
        //    var uhrzeitString: String
        var wind: String?
        var tagebuchseite: Tagebuchseite?

        init(aktivitaet: String = "", fauna: String = "", flora: String = "", himmel: String = "", ort: String = "", temperatur: String = "", uhrzeit: String = "", wind: String = "") {
            self.aktivitaet = aktivitaet
            self.fauna = fauna
            self.flora = flora
            self.himmel = himmel
            self.ort = ort
            self.temperatur = temperatur
            self.uhrzeit = uhrzeit
            //        self.uhrzeitString = uhrzeitString
            self.wind = wind
        }

    }
}

enum TagebuchseiteSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2,0,0)

    static var models: [any PersistentModel.Type] {[Tagebuchseite.self]}

    @Model
    class Tagebuchseite {
        @Attribute(.unique) var datum: Date
        var datumString: String
        var tagebuchseiteId: UUID
        @Relationship(deleteRule: .cascade, inverse: \Tagebucheintrag.tagebuchseite) var tagebucheintraege: [Tagebucheintrag]?

        init(datum: Date = .now, datumString: String = Date.now.formatted(date: .numeric, time: .omitted), tagebuchseiteId: UUID = UUID(), tagebucheintraege: [Tagebucheintrag] = [Tagebucheintrag]()) {
            self.datum = datum
            self.datumString = datumString
            self.tagebuchseiteId = tagebuchseiteId
            self.tagebucheintraege = tagebucheintraege
        }
    }

    @Model
    class Tagebucheintrag {
        var aktivitaet: String?
        var fauna: String?
        var flora: String?
        var himmel: String?
        var ort: String?
        var temperatur: String?
        var uhrzeit: String?
        //    var uhrzeitString: String
        var wind: String?
        var tagebuchseite: Tagebuchseite?

        init(aktivitaet: String = "", fauna: String = "", flora: String = "", himmel: String = "", ort: String = "", temperatur: String = "", uhrzeit: String = "", wind: String = "") {
            self.aktivitaet = aktivitaet
            self.fauna = fauna
            self.flora = flora
            self.himmel = himmel
            self.ort = ort
            self.temperatur = temperatur
            self.uhrzeit = uhrzeit
            //        self.uhrzeitString = uhrzeitString
            self.wind = wind
        }

    }

}

enum TagebuchseiteSchemaV3: VersionedSchema {
    static var versionIdentifier = Schema.Version(3,0,0)

    static var models: [any PersistentModel.Type] {[Tagebuchseite.self]}

    @Model
    class Tagebuchseite {
        @Attribute(.unique) var datum: Date
        var datumString: String?
        var tagebuchseiteId: UUID
        @Relationship(deleteRule: .cascade, inverse: \Tagebucheintrag.tagebuchseite) var tagebucheintraege: [Tagebucheintrag]?
//        var testString: String

        init(datum: Date = .now, datumString: String = Date.now.formatted(date: .numeric, time: .omitted), tagebuchseiteId: UUID = UUID(), tagebucheintraege: [Tagebucheintrag] = [Tagebucheintrag]()) {
            self.datum = datum
            self.datumString = datumString
            self.tagebuchseiteId = tagebuchseiteId
            self.tagebucheintraege = tagebucheintraege
//            self.testString = testString
        }
    }

    @Model
    class Tagebucheintrag {
        var aktivitaet: String?
        var fauna: String?
        var flora: String?
        var himmel: String?
        var ort: String?
        var temperatur: String?
        var uhrzeit: String?
        //    var uhrzeitString: String
        var wind: String?
        var tagebuchseite: Tagebuchseite?

        init(aktivitaet: String = "", fauna: String = "", flora: String = "", himmel: String = "", ort: String = "", temperatur: String = "", uhrzeit: String = "", wind: String = "") {
            self.aktivitaet = aktivitaet
            self.fauna = fauna
            self.flora = flora
            self.himmel = himmel
            self.ort = ort
            self.temperatur = temperatur
            self.uhrzeit = uhrzeit
            //        self.uhrzeitString = uhrzeitString
            self.wind = wind
        }

    }

}

enum TagebuchseiteSchemaV4: VersionedSchema {
    static var versionIdentifier = Schema.Version(4,0,0)

    static var models: [any PersistentModel.Type] {[Tagebuchseite.self]}

    @Model
    class Tagebuchseite {
        @Attribute(.unique) var datum: Date
        var datumString: String?
        var tagebuchseiteId: UUID
        @Relationship(deleteRule: .cascade, inverse: \Tagebucheintrag.tagebuchseite) var tagebucheintraege: [Tagebucheintrag]?
        var testString: String?

        init(datum: Date = .now, datumString: String = Date.now.formatted(date: .numeric, time: .omitted), tagebuchseiteId: UUID = UUID(), tagebucheintraege: [Tagebucheintrag] = [Tagebucheintrag](), testString: String = "Ein String") {
            self.datum = datum
            self.datumString = datumString
            self.tagebuchseiteId = tagebuchseiteId
            self.tagebucheintraege = tagebucheintraege
            self.testString = testString
        }
    }

    @Model
    class Tagebucheintrag {
        var aktivitaet: String?
        var fauna: String?
        var flora: String?
        var himmel: String?
        var ort: String?
        var temperatur: String?
        var uhrzeit: String?
        var wind: String?
        var tagebuchseite: Tagebuchseite?

        init(aktivitaet: String = "", fauna: String = "", flora: String = "", himmel: String = "", ort: String = "", temperatur: String = "", uhrzeit: String = "", wind: String = "") {
            self.aktivitaet = aktivitaet
            self.fauna = fauna
            self.flora = flora
            self.himmel = himmel
            self.ort = ort
            self.temperatur = temperatur
            self.uhrzeit = uhrzeit
            self.wind = wind
        }

    }

}

enum TagebuchseiteMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [TagebuchseiteSchemaV1.self, TagebuchseiteSchemaV2.self, TagebuchseiteSchemaV3.self, TagebuchseiteSchemaV4.self]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: TagebuchseiteSchemaV1.self,
        toVersion: TagebuchseiteSchemaV2.self,
        willMigrate: {
            context in
            let tagebuchseiten = try context.fetch(FetchDescriptor<TagebuchseiteSchemaV1.Tagebuchseite>())
            var usedDatumStrings = Set<String>()

            for tagebuchseite in tagebuchseiten {
                if usedDatumStrings.contains(tagebuchseite.datumString) {
                    context.delete(tagebuchseite)
                }
                usedDatumStrings.insert(tagebuchseite.datumString)
            }

            try context.save()
        },
        didMigrate: nil
    )

    static let migrateV2toV3 = MigrationStage.lightweight(
        fromVersion: TagebuchseiteSchemaV2.self,
        toVersion: TagebuchseiteSchemaV3.self
    )

    static let migrateV3toV4 = MigrationStage.lightweight(
        fromVersion: TagebuchseiteSchemaV3.self,
        toVersion: TagebuchseiteSchemaV4.self
    )

    static var stages: [MigrationStage] {
        [migrateV3toV4]
    }
}

extension Tagebuchseite {
    @MainActor
    static var preview: ModelContainer {
        let container = try! ModelContainer(for: Tagebuchseite.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
        return container
    }
}
//
//  ExtensionTagebucheintrag.swift
//  Tagebuch v5
//
//  Created by Ralf 2 on 15.12.23.
//

import Foundation

extension Tagebucheintrag {

    public var wrappedAktivitaet : String {
        aktivitaet ?? "Keine Aktivität"
    }

    public var wrappedFauna : String {
        fauna ?? "Keine Angabe"
    }

    public var wrappedFlora : String {
        flora ?? "Keine Angabe"
    }

    public var wrappedOrt : String {
        ort ?? "Keine Angabe"
    }

    public var wrappedHimmel : String {
        himmel ?? "Keine Angabe"
    }

    public var wrappedWind : String {
        wind ?? "Keine Angabe"
    }

    public var wrappedUhrzeit : String {
        uhrzeit ?? "Keine Angabe"
    }

    public var wrappedTemperatur : String {
        temperatur ?? "Keine Temperaturangabe"
    }

}
//
//  ContentView.swift
//  Tagebuch v5
//
//  Created by Ralf 2 on 15.12.23.
//

import SwiftUI
import SwiftData

struct ContentView: View {

    @Environment(\.modelContext) var modelContext

    @State private var sortOrder = SortDescriptor(\Tagebuchseite.datum, order: .reverse)
    @Query var tagebuchseiten: [Tagebuchseite]

    @State private var path = [Tagebuchseite]()
    @State private var isAlertPresented = false
    @State private var searchText = ""

    var body: some View {

        NavigationStack(path: $path) {
            TagebuchseiteListingView(sort: sortOrder, searchString: searchText)
            .navigationTitle("Tagebuch")
            .searchable(text: $searchText, prompt: "Datum, numerische Angabe")
            .navigationDestination(for: Tagebuchseite.self, destination: EditingTagebuchseiteView.init)
            .toolbar {
                EditButton()
                    .environment(\.locale, Locale(identifier: "de"))
                Button("Daten eingeben", systemImage: "calendar.badge.plus", action: addTagebuchseite)
            }
            .alert("Datum vorhanden", isPresented: $isAlertPresented) {
                Button("Abbrechen", role: .cancel) {
                    clear()
                }
            } message: {
                Text("Das Datum ist schon vorhanden. Bitte bestehenden Eintrag verwenden")
            }
            .toolbar {
                ToolbarItem {
                    ControlGroup {
                        Button("Init new property", systemImage: "1.square") {
                            initTestString()
                        }
                    }.controlGroupStyle(.navigation)
                }
            }

        }
    }

    func initTestString() {
        for tagebuchseite in tagebuchseiten {
            if tagebuchseite.testString == nil {
                tagebuchseite.testString = "Mit Button gesetzt"
            }
        }
    }

    func addTagebuchseite() {
        let neuesDatum = Date()
        var existiertBereits = false

        for tagebuchseite in tagebuchseiten {
            if Calendar.current.isDate(tagebuchseite.datum, equalTo: neuesDatum, toGranularity: .day) {
                existiertBereits = true
                break
            }
        }

        if existiertBereits {
            isAlertPresented = true
        } else {
            let neueTagebuchseite = Tagebuchseite()
            modelContext.insert(neueTagebuchseite)
            path = [neueTagebuchseite]
        }
    }

    func clear() {
        searchText = ""
        isAlertPresented = false

    }

    func addSamples() {

//        Definition der Beispieldaten
        let heute  = Date.now
        let vorgestern = Date.now - (2 * 86400)
//        let gestern = heute - 86400

        let TBheute = Tagebuchseite(datum: heute, datumString: heute.formatted(date: .numeric, time: .omitted))
        let TBgestern = Tagebuchseite(datum: heute - 86400, datumString: "14.12.2023")
        let TBvorgestern = Tagebuchseite(datum: vorgestern, datumString: vorgestern.formatted(date: .numeric, time: .omitted))

        modelContext.insert(TBvorgestern)
        modelContext.insert(TBgestern)
        modelContext.insert(TBheute)
    }

}

#Preview {
    ContentView()
}

1      

Is this in your code or you just pasted for preview here like so?

static var stages: [MigrationStage] {
        [migrateV3toV4]
    }

it should contain all stages

static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3, migrateV3toV4]
    }

1      

Theoretically yes. But if I include all stages it does not work. Step by step, meaning to have only one stage, it works until V3

1      

But for migration to work stages must include all versions, otherwise it will not know how to migrate from V1 onwards, seem like there is fundamental issue in the setup itself. I would also recommend to revise the model setup itself. Well this is my personal opinion. But what is the point intializing string with empty value, if you have them as optional? You may think to put just nil value instead. Other point you have datumString and have it initialzed upon creating of objects from Data(). But you have already property datum which is the Date type, why do you need to store datumString, you can just create computed property in model which is kind of transient data and will not be saved to persistent store. But whenever you access it you can have it computed from the datum property which is stored. I would advise to create some mock data and start with V1, save it in simulator then add V2, try to migrate and so on. So I think this will show where is the issue starts and what is the reason for that error.

1      

Yeah. Thanks for the really good ideas. I'll follow them during the next days . I will need some time, because coding is my sparetime activity.

Some of your remarks are still true. Sometimes the reason is just progress in learning - or making a workaround because I hadn't the knowledge at that time.

Will keep you informed.

Many thanks, Ralf

1      

Matkaa.center is one of the leading and most trusted website in the Satta Matka Gaming Industry. Matkaa.center provide you all the information that you need to win the game.

   

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.