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 of type [Data] to the new memory.photos of type [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 {
@Environment(\.scenePhase) var scenePhase
@AppStorage("writingRemindersEnabled") var writingRemindersEnabled: Bool = false
let container: ModelContainer
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)
}
}