Post

Replies

Boosts

Views

Activity

Reply to SwiftData + CloudKit process for deduplication / consuming relevant store changes?
I'd like to respond to DelawareMathGuys's suggestion to use fatbobman's SwiftDataKit to implement history processing, but it is too big to be in reply to a root comment so I'm putting it here. Fatbobman has written some great blog posts about SwiftData, and it deserves a real response. I have actually already implemented fabobman's approach in a dev branch of my project. I don't think it's viable for production in a commercial app for a few reasons: In a Core Data history processor you can execute a query with a predicate, but in FBM's hack, you cannot, because SwiftData itself does not support passing predicates to the history fetch. Normally you set the transaction author to be something like "app" or "widget" and if the author is missing you can assume it is coming from CloudKit. So you query your history for author != excluded authors, and you can process relevant changes from the network. It is impossible to do any pre-filtering on the transaction so you have to process every transaction that has happened and filter in memory. let fetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: timestamp) // In SwiftData, the fetchRequest.fetchRequest created by fetchHistory is nil and predicate cannot be set. You can't set merge policies (like NSMergeByPropertyObjectTrumpMergePolicy) in SwiftData using FBM's approach, so you can't easily control how the merge happens. His approach is fully based on his SwiftDataKit extensions, which is based entirely on undocumented internal implementation details of SwiftData. For example, to get a SwiftData PersistentIdentifier, he makes a mock Codable struct that can be populated with undocumented elements of a Core Data NSManagedObject to build a struct that can be encoded to JSON, that can be decoded back to a SwiftData PersistentIdentifier. So it depends on the undocumented structure of the PersistentIdentifier and its relationship to the underlying Core Data object. That's probably stable... // from https://github.com/fatbobman/SwiftDataKit/blob/main/Sources/SwiftDataKit/CoreData/NSManagedObjectID.swift // Compute PersistentIdentifier from NSManagedObjectID public extension NSManagedObjectID { // Compute PersistentIdentifier from NSManagedObjectID var persistentIdentifier: PersistentIdentifier? { guard let storeIdentifier, let entityName else { return nil } let json = PersistentIdentifierJSON( implementation: .init(primaryKey: primaryKey, uriRepresentation: uriRepresentation(), isTemporary: isTemporaryID, storeIdentifier: storeIdentifier, entityName: entityName) ) let encoder = JSONEncoder() guard let data = try? encoder.encode(json) else { return nil } let decoder = JSONDecoder() return try? decoder.decode(PersistentIdentifier.self, from: data) } } // Extensions to expose needed implementation details extension NSManagedObjectID { // Primary key is last path component of URI var primaryKey: String { uriRepresentation().lastPathComponent } // Store identifier is host of URI var storeIdentifier: String? { guard let identifier = uriRepresentation().host() else { return nil } return identifier } // Entity name from entity name var entityName: String? { guard let entityName = entity.name else { return nil } return entityName } } So, as I worked on trying his approach, I felt that it was a clever hack that I wouldn't be comfortable depend on in production, to ultimately implement a solution that isn't very good (request all transactions from all sources and filter in memory without being able to set a merge policy for the final set of transactions). I think what he made is a neat workaround, but for a commercial app I think it would be better to implement the parallel Core Data stack and do real history change processing. Or fix the gaps discussed above with unavailable predicates and merge policies. But best of all would be a mechanism to do this in SwiftData itself.
Feb ’24
Reply to SwiftData does not work on a background Task even inside a custom ModelActor.
It turns out that it's not just the context that runs on the main thread, but the actor appears to be isolated to the main thread as well. If we create a normal actor, it runs on thread from the thread pool (not the main thread). However, if we create a ModelActor, it appears to inherit the thread from its parent. You can test this with Thread.isMainThread. @ModelActor final actor MyActor { var contextIsMainThread: Bool? var actorIsMainThread: Bool? func determineIfContextIsMainThread() { try? modelContext.transaction { self.contextIsMainThread = Thread.isMainThread } } func determineIfActorIsMainThread() { self.actorIsMainThread = Thread.isMainThread } } As has been discussed above, you get this behavior based on how your model actor is initiated. content .onAppear { actorCreatedOnAppear = MyActor(modelContainer: mainContext.container) // actor and modelContext are on main thread Task { self.actorCreatedOnAppearTask = MyActor(modelContainer: mainContext.container) // actor and modelContext are on main thread } Task.detached { self.actorCreatedOnAppearDetachedTask = MyActor(modelContainer: mainContext.container) // actor and modelContext are NOT on main thread. This is the only option which matches the behavior of other Swift actors. } } I've submitted FB13450413 with a project demonstrating this.
Dec ’23
Reply to Do I need to add my own unique id?
I suspect that the persistent identifier is a wrapper for the underlying Core Data objectID: NSManagedObjectID. The caveat with using the core data NSManagedObjectID is that it is unique, and permanent, EXCEPT when the object is first created and before it is persisted. There is an initial temporary objectID assigned that is later replaced with the permanent id. If you want to use the SwiftData PersistentIdentifier, I would recommend checking that this behavior does or does not transfer over from Core Data. Personally, I've been burned enough by the complexity of dealing with that Core Data NSManagedObjectID that I just create an id: UUID on my objects rather than rely on the persistence layer id.
Aug ’23
Reply to Xcode 15 beta 5 issue when using @Query data to update Button label
I'm seeing a related issue in my app as well. I also have a query that I overwrite a view init similar to your _favorites = Query(filter: filter, sort: \FavoriteModel.createdDate) However, very strangely, I have an interaction with my TabView navigation. I have a navigator class @Observable public final class Navigator { public var route: Route? = nil } and my tabview binds to it TabView(selection: $navigator.route) { Text("view 1") .tabItem { Label("Recent", systemImage: "chart.bar.xaxis") } .tag(Route.locationDetail as Route?) } If I instead get rid of the Navigator, and just bind to a local @State var route: Route in the view, then it eliminates the problem in one app. In another app we've got IP, we just have to comment out the = Query(...) change in the view initializer (thereby breaking the view)
Jul ’23
Reply to SwiftData background inserts using ModelActor do not trigger SwiftUI @Query view updates, but background deletes do
In Xcode 15 beta 5 this behavior is perhaps fixed? My sample project does see view refreshes when adding an item via a ModelActor. However, after creating and deleting a few Items, the List will suddenly only show a single Item. If I relaunch the app, then all Items will appear again. It seems like maybe we are getting progress on fixing the ModelActor view refreshes (great!) but there are still some bugs. BTW my sample code above needs a few small tweaks to run on Xcode 15 beta 5. ContentView import SwiftUI import SwiftData struct ContentView: View { @Environment(\.modelContext) private var modelContext @Query private var items: [Item] @State private var simpleModelActor: SimpleModelActor! var body: some View { NavigationView { List { ForEach(items) { item in NavigationLink { Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") } label: { Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) } } .onDelete(perform: deleteItems) } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { EditButton() } ToolbarItem { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } Text("Select an item") } .onAppear { simpleModelActor = SimpleModelActor(modelContainer: modelContext.container) } } private func addItem() { Task { await simpleModelActor.addItem() } } private func deleteItems(offsets: IndexSet) { Task { for index in offsets { await simpleModelActor.delete(itemWithID: items[index].objectID) } } } } #Preview { ContentView() .modelContainer(for: Item.self, inMemory: true) } SimpleModelActor import Foundation import SwiftData final actor SimpleModelActor: ModelActor { let executor: any ModelExecutor init(modelContainer: ModelContainer) { let modelContext = ModelContext(modelContainer) executor = DefaultModelExecutor(context: modelContext) } func addItem() { let newItem = Item(timestamp: Date()) context.insert(newItem) try! context.save() // this does not impact a re-display by the @Query in ContentView. I would have expected it to cause a view redraw. } func delete(itemWithID itemID: Item.ID) { let item = context.object(with: itemID) context.delete(item) // this DOES cause a view redraw in ContentView. It triggers an update by @Query. // try! context.save() // this makes do difference to view redraw behavior. } } Item import Foundation import SwiftData @Model final class Item: Identifiable { var timestamp: Date init(timestamp: Date) { self.timestamp = timestamp } } And of course the main App file needs .modelContainer(for: Item.self) added to the window group.
Jul ’23
Reply to WeatherKit error on iOS 17 - macOS 14
I'm seeing the same issue. I created a minimal WeatherKit app and it runs fine on Xcode 15 beta 4 using a iOS 16.4 simulator. On an iOS 17 beta 3 simulator it fails with the following error message: Aborting silent interpolation: missing cached hourlyForecast; location=CLLocationCoordinate2D(latitude: 35.157019886059835, longitude: -85.34119394760656) Failed to generate jwt token for: com.apple.weatherkit.authservice with error: Error Domain=WeatherDaemon.WDSJWTAuthenticatorServiceListener.Errors Code=2 "(null)" Encountered an error when fetching weather data subset; location=<+35.15701989,-85.34119395> +/- 0.00m (speed -1.00 mps / course -1.00) @ 7/13/23, 8:29:48 PM Eastern Daylight Time, error=WeatherDaemon.WDSJWTAuthenticatorServiceListener.Errors 2 Error Domain=WeatherDaemon.WDSJWTAuthenticatorServiceListener.Errors Code=2 "(null)" I've filed this as feedback FB12602396
Jul ’23
Reply to ImageRenderer fails to render contents of ScrollViews
I wanted to close this out. I got a response to my Feedback indicating that this is the expected behavior. ImageRenderer output only includes views that SwiftUI renders, such as text, images, shapes, and composite views of these types. It does not render views provided by native platform frameworks (AppKit and UIKit) such as web views, media players, and some controls. For these views, ImageRenderer displays a placeholder image, similar to the behavior of drawingGroup(opaque:colorMode:). I wish there was a way for me to know which views are provided by native frameworks so I could predict if ImageRenderer would fail, but there you go.
Jul ’23