TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Struggling with SwiftData Migration

Forums > SwiftUI

I'll preface by saying I'm a new Swift developer, having a go at my first app. Just a simple memory tracker. I'm building it using SwiftData + iCloud Syncing. I had set up my model like this:

@Model
final class Memory {
    var content: String = ""
    var dateCreated: Date = Date.now
    var dateUpdated: Date = Date.now
    var tags: [Tag]? = [Tag]()
    @Attribute(.externalStorage) var images: [Data] = [Data]()

    init(
        content: String = "",
        dateCreated: Date = .now,
        dateUpdated: Date = .now,
        tags: [Tag] = [Tag](),
        images: [Data] = [Data]()
    ) {
        self.content = content
        self.dateCreated = dateCreated
        self.dateUpdated = dateUpdated
        self.tags = tags
        self.images = images
    }
}

But I discovered that led to a massive performance issue as soon as someone added a few images to a Memory. Maybe SwiftData isn't correctly putting an ARRAY of Data into external storage? My memory usage would just balloon with each photo added. All the examples I've seen just use a singular Data type for external storage, so not sure.

Anyway, I played around with different options and figured out that a new MemoryPhoto struct was probably best, so I put the old model in a V1 schema and my NEW V2 model looks like this...

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

    static var models: [any PersistentModel.Type] {
        [Memory.self, Tag.self, MemoryPhoto.self]
    }

    @Model
    final class Memory {
        var content: String = ""
        var dateCreated: Date = Date.now
        var dateUpdated: Date = Date.now
        var tags: [Tag]? = [Tag]()
        @Relationship(deleteRule: .cascade) var photos: [MemoryPhoto]? = [MemoryPhoto]()
        @Attribute(.externalStorage) var images: [Data] = [Data]()

        init(
            content: String = "",
            dateCreated: Date = .now,
            dateUpdated: Date = .now,
            tags: [Tag] = [Tag](),
            images: [Data] = [Data](),
            photos: [MemoryPhoto] = [MemoryPhoto]()
        ) {
            self.content = content
            self.dateCreated = dateCreated
            self.dateUpdated = dateUpdated
            self.tags = tags
            self.images = images
            self.photos = photos
        }
    }

    @Model
    final class MemoryPhoto {
        @Attribute(.externalStorage) var originalData: Data?
        @Relationship(inverse: \Memory.photos) var memory: Memory?

        init(originalData: Data? = nil, memory: Memory? = nil) {
            self.originalData = originalData
            self.memory = memory
        }
    }

Here's my migration, currently, which does work, because as best I can tell this is a lightweight migration...

enum DataMigrationPlan: SchemaMigrationPlan {

    static var schemas: [any VersionedSchema.Type] {
        [DataSchemaV1.self, DataSchemaV2.self]
    }

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

    static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: DataSchemaV1.self, toVersion: DataSchemaV2.self)

}

But what I'm trying to figure out now is to migrate the former memory.images: [Data] to the new memory.photos: [MemoryPhoto] and been struggling. Any type of custom migration I do fails, sometimes inconsistently. I can try to get the exact errors if helpful but at this point not even a simple fetch to existing memories and updating their content as a part of the migration works.

Is there a way to write a hypothetical V2 to V3 migration that just takes the images and puts them in the photos "slot"? For instance, what I do have working is this function that basically runs a "migration" or sorts when a given memory appears and it has the former images property.

....
        .onAppear {
            convertImagesToPhotos()
        }
    }

    private func convertImagesToPhotos() {
        guard !memory.images.isEmpty && memory.unwrappedPhotos.isEmpty else { return }

        let convertedPhotos = memory.images.map { imageData in
            MemoryPhoto(originalData: imageData)
        }
        memory.photos?.append(contentsOf: convertedPhotos)

        memory.images.removeAll()
    }

Any help or pointers appreciated for this newbie swift developer. If helpful, here's the main App struct too...

@main
struct YesterdaysApp: App {

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

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

2      

I tried this as well a bit and struggled. It's not that you are a newbie, you are a victim to immature technologies.

1      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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.