Wondering if anyone else is running into this.
It seems ModelConfiguration(isStoredInMemoryOnly: true) for previews (as outlined by Paul Hudson / Hacking with Swift) works correctly for
iOS + iCloud syncing
macOS WITHOUT iCloud syncing
But as soon as I turn on iCloud syncing capability for my macOS target, its as if the isStoredInMemoryOnly has no effect on the macOS target.
Here's my code...
I made a PreviewHelper to encapsulate the preview logic...
enum PreviewHelper {
static let previewModelContainer: ModelContainer = {
do {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Task.self, configurations: config)
return container
} catch {
fatalError("Failed to create model container for previewing: \(error.localizedDescription)")
}
}()
}
And then use it like so...
#Preview {
let container = PreviewHelper.previewModelContainer
for task in MockData.tasks {
container.mainContext.insert(task)
}
return HorizonView()
.modelContainer(container)
}
On the macOS target & destination, using a macOS device in the Preview canvas, with iCloud syncing turned on that code inserts the MockData.tasks into my iCloud container every time the preview refreshes, so the data just keeps getting duplicated. With iCloud syncing turned off it behaves as expected/correctly (just inserting the MockData as needed for Previews).
In an iOS target, using the same helper and mock data, the helper behaves as expected/correctly (with or without iCloud syncing enabled).
Assuming this might be a bug/oversight with SwiftData and macOS? Or am I missing a needed configuration/capability on the macOS side? Anybody else seeing this?
Post
Replies
Boosts
Views
Activity
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)
}
}