iOS15: NSPersistentCloudKitContainer: how to un-share objects? - ios

I’m working on an app that uses the new sharing support in iOS 15 using NSPersistentCloudKitContainer. I do not see a way to tell Core Data that an object, or set of objects, is no longer shared.
For example, if I create a set of objects and then call share(_:to:completion:), the objects are shared properly and moved to a new, custom CKRecordZone, as expected.
Now, if the user stops sharing an object using UICloudSharingController, the CKShare that Core Data created is properly deleted from CloudKit, but the objects are still in the custom zone, and Core Data still has the original CKShare associated with those objects. So, when I call fetchShares(matching:), I still get the CKShare, but of course, that is no longer valid. In the past, with my own code, I’d use UISharingControllers delegate to get notified that the user stopped sharing, and then update my model. But there doesn’t seem to be a way to tell Core Data about the change.
Forcing Core Data to fetch CloudKit changes by either moving the app to background and then foreground, or by stopping the app and relaunching does not cause Core Data to notice the change to the share.
Does anyone know how to tell Core Data that these objects are no longer shared?

I worked around this by always checking to see if the share actually exists in CloudKit, rather than relying on the existence of a CKShare from fetchShares(matching:). I get the URL from the CKShare returned from fetchShares(matching:) and call this:
private func remoteShare(at url: URL) async throws -> CKShare? {
do {
let metadata = try await cloudKitContainer.shareMetadata(for: url)
return metadata.share
} catch let error as CKError {
if error.retryable {
throw RemoteError.retryable
} else if error.userInterventionRequiredError {
throw RemoteError.userInterventionRequired
} else if error.code == .unknownItem {
return nil
} else {
throw RemoteError.remoteFailure
}
} catch {
throw RemoteError.remoteFailure
}
}
}
If I get unknownItem that means there is no share on the remote, so the object is not actually shared.
The RemoteError is my custom error handling, and I have some extensions on CKError to categorize the errors.

Another workaround to the issue is to create a copy of the object being shared, and then deleting the original. It's not elegant, but it works in the meantime. (Hopefully Apple will address this at some point with a better solution.)
The copy will be placed in the default zone like it was before you shared the original. You'll be left with an empty share zone, but I dealt with that by running a background task upon app launch that deletes all empty share zones, so it doesn't get out of hand.
I haven't tried the solution posted by Dharman, but I imagine it's slower than querying the local cache. Using the method above, you can still use the "isShared" code from the demo app.

Related

Can I make realm.writes directly in the Object data model?

I'm writing an app using Realm data persistence of certain objects.
In an attempt to clean up/remodel my code (getting realm.writes out of the Views and Controllers), I tried to put them directly in the persisted object class.
The logic is basically this:
class PersistedObject: Object {
public var data: String {
get { _saved_data }
set {
do { try realm?.write { _saved_data = newValue }
} catch { print(error) }
}
}
#objc dynamic private var _saved_data = "hello there"
}
This way, I'd be able to access and rewrite realm object properties from view controllers, without needing realm.writes directly in there. That's the idea, anyway.
This works sometimes. Other times, the app crashes with the error...
"Realm accessed from incorrect thread"
...which is what I'm currently trying to solve.
This is my first iOS app and my first time using Realm.
Does it make sense to organize the code like this (I've found little in terms of support in this approach, but also generally little at all, in terms of MVC best-practices when working with Realm)
If it does make sense, how can I solve the problem with accessing Realm from the incorrect thread, while still doing the realm.writes directly in the object class?
Thanks in advance! :)
Simon
There is no sense to organize code like this. You will be able to write only from same thread it was created
to modify objects from different thread you can use ThreadSafeReference for example
You're not going to want to do that.
There's no reason not to realm.write whenever you want to write to realm - that's what it's there for. This pattern works:
// Use them like regular Swift objects
let myDog = Dog()
myDog.name = "Rex"
myDog.age = 1
// Get the default Realm
let realm = try! Realm()
// Persist your data easily
try! realm.write {
realm.add(myDog)
}
Obviously there should be better error catching in the above code.
Another downside is if you want to write 10 objects, they are written as soon as the data property is set - what if there are three vars you want to set and heep in memory before writing it? e.g. your user is creating a list of items in your app - if the user decides not to do that and hit's Cancel, you would then have to hit the database again to delete the object(s).
Consider a case where you want to write 10 objects 'at the same time'?
realm.add([obj0, obj1, obj2...])
is a lot cleaner.
Another issue comes up if you want to guarantee objects are written within a transaction - either it all succeeds or all fails. That can't be done with your current object.
The last issue is that often you'll want to instantiate an object and add some data to it, populating the object before writing to realm. With the code in the question, you're writing it as soon as data is populated. You would have to add that same code to every property.

How to fix connection close issue in synchronised method?

In our application, we are using grails framework and SQL server for database. We have multiple sites and those sites can have multiple users (a few users) and if they are accessing the same method via AJAX that can cause issue so we made the that method as synchronized method and to minimize the database interaction we are storing data in map on site basis since all the user from one site will get the same data, and if the data is older than 10 seconds we get the data from database and update the map object. Here we are getting a lot of database connection close issues on the very first line of synchronized method where we are getting site object from database. What is the issue here and how we can resolve the issue?
def synchronized getData(params){
Site site = Site.get(params.siteId)
// Here we are checking whether site data does not exists in map
// or the data expired (10 second older data) then we get data from
// database and update the map object
// Then here we create new list object from the data in map object
return list
}
Difficult to figure out the exact problem without more information here. Several things stand out...
I'm not especially familiar with using the synchronized keyword in front of a service method, I would recommend trying the synchronized annotation with a static object key:
private static final myLock = new Object()
#Synchronized("myLock")
void getData() {
//do stuff
}
or synchronizing explicitly within the method
void getData() {
synchronized(myLock) {
//do stuff
}
}
I don't know if that's related to your connection closing issues, but worth a try.
But also notably, grails and hibernate provide caching of database retrieves, so if you're loading the same data that's been loaded into hibernate cache, you don't need to cache this in a Map locally... grails is already doing that for you. Site site = Site.get(params.siteId) will NOT make a database call if it's been called recently and is already cached by the framework.
I would strongly suggest running some performance checks just making that call vs. caching in a Map object, especially if you're expiring in ~10s anyway.

CoreStore create object in context without saving to database

I want to solve next problem:
I would like to work with some NSManagedObject in context and change some properties in runtime, but without telling SQLite about any changes in it.
I just want to save NSManagedObject to database when I hit save button or similar.
As I found out from source code demo we need to use beginUnsafe for this purposes (maybe I am wrong)
func unstoredWorkout() -> WorkoutEntity {
let transaction = CoreStore.beginUnsafe()
let workout = transaction.create(Into<WorkoutEntity>())
return workout
}
let workout = unstoredWorkout()
workout.muscles = []
Now when I try to update workout.muscles = [] app crashes with error:
error: Mutating a managed object 0x600003f68b60 <x-coredata://C00A3E74-AC3F-47FD-B656-CA0ECA02832F/WorkoutEntity/tC3921DAE-BA43-45CB-8271-079CC0E4821D82> (0x600001c2da90) after it has been removed from its context.
My question how we can create object without saving it and how we can save it then when we modify some properties and avoid this crash as well.
The reason for the crash is that your transaction only lives in your unstoredWorkout() method, so it calles deinit, which resets the context (and deletes all unsaved objects).
You have to retain that unsafe transaction somewhere to keep your object alive - such as in the viewcontroller that will eventually save the changes.
But I would rather encourage you to think about that if you really want to do that. You might run into other synchronization issues with various context or other async transactions alive, like when API calls are involved.

Realm invalidated error even if invalidated == false

I know that Realm can crash easily with invalidated object... however, I try to catch such state before pursuing with a given object and it usually works.
However, for my Request object sometimes I get a crash on realm.add(self) with "Adding a deleted or invalidated object to a Realm is not permitted".
I am not sure whether invalidated is a false negative or if my "custom" way of verifying an object has been deleted is wrong (I'm just adding the id of the object in a shared dictionary before actually deleting it).
I am a bit stuck on that one and it causes crashes in my app :(
if self.invalidated == false{
if let deleted = RequestHelper.sharedHelper.deletedRequests[id] where deleted == true{
return
}
let realm = try! Realm()
do{
try realm.write{
realm.add(self)
}
}catch{}
id = self.id
}
else{
print("realm invalidation")
}
I'm a little confused about the logic you're using here. You use realm.add() to add a new object to a Realm instance for the first time. Before then, checking if it's invalidated before that will most likely always return false because it shouldn't be backed by a Realm at that point yet.
If it's already backed by a Realm instance, it's also important to note that Realm write transactions happen sequentially. Only one may be open at a time, and each one after that will wait until the current one has finished. That being the case, it's feasible that self.invalidated was indeed false, but by the time you've opened the write transaction here, another write transaction may have just deleted it.
My recommendation would be to try and rely on Realm itself as much as possible for checking the deleted state of objects instead of relying on a custom mechanism. If id has been set as a primary key, you can use realm.objectForPrimaryKey(ObjectType.self, key: id) to very quickly check if it already exists in the Realm instance or not (Instead of trying to manage a separate list yourself).
Please let me know if you need any extra clarification.

Core Data: how to just delete and rebuild the data store?

I'm using Core Data in an iOS 7+ app that does not need to save user's data, all data the app needs is requested to services and it can be recovered at any time. So, if I change my data model in a next app update, I have no problem in deleting all the previous data and requesting it all again. But I don't know how to simply replace the previous data model with the new one, without performing a migration since it looks that I don't need to do that...
Thanks in advance
Working Swift solution if you target iOS 9 or higher
The shared CoreData manager:
class CoreDataContext {
static let datamodelName = "CoreDataTests"
static let storeType = "sqlite"
static let persistentContainer = NSPersistentContainer(name: datamodelName)
private static let url: URL = {
let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("\(datamodelName).\(storeType)")
assert(FileManager.default.fileExists(atPath: url.path))
return url
}()
static func loadStores() {
persistentContainer.loadPersistentStores(completionHandler: { (nsPersistentStoreDescription, error) in
guard let error = error else {
return
}
fatalError(error.localizedDescription)
})
}
static func deleteAndRebuild() {
try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: storeType, options: nil)
loadStores()
}
}
call loadStores only once in the appDelegate and deleteAndRebuild when you want to delete and rebuild the database :)
Case 1: You're using a SQLite store
This applies if your store type is NSSQLiteStoreType. Even if you plan to delete your data from time to time, it's not a bad idea to stick to SQLite, as it gives you the flexibility to keep your cached data on disk as long as you wish, and only delete it when you change your model and you don't want to apply any migrations.
Quick solution? Delete your NSPersistentStoreCoordinator's store at startup, when you're initializing Core Data.
For instance, if you're using the default SQLite store, provided by Apple's boilerplate code:
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"cd.sqlite"]
you can simply delete the file:
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
then use storeURL to add a new persistent store as usual.
Your NSPersistentStoreCoordinator won't complain if you have a new model and you won't need any migrations, but your data will be lost, of course.
It goes without saying that you can decide to use this solution when you detect a change in the data model, leaving the store alone if there's no change, so that you can preserve your cached data as long as necessary.
UPDATE:
As suggested by TomHerrington in the comments, to be sure you've removed the old store completely, you should also delete journal files, which might come back and haunt you in the future, if you don't take care of them.
If your store file is named cd.sqlite, like in the example, the additional files to be removed are cd.sqlite-shm and cd.sqlite-wal.
WAL journaling mode for Core Data was introduced as the default in iOS 7 and OSX Mavericks, as reported by Apple in QA1809.
Case 2: Use an in-memory store
As suggested, you could switch to an in-memory store, using NSInMemoryStoreType instead of NSSQLiteStoreType. Erasing the store is much easier in this case: all your data resides in memory, all of it will disappear when your app stops running, nothing will remain on disk for you to clean. Next time, you could potentially load a completely different model, without any migrations, as there's no data to migrate.
However, this solution, implemented as it is, wouldn't let you cache data between sessions, which looks like something you'd like to do between app updates (i.e., the store must be erased only when the app is updated and the model changes, while it could be useful to keep it on disk otherwise).
Note:
Both approaches are viable, with their pros and cons, and I'm sure there could be other strategies as well. In the end, you should have all the elements to decide what the best approach would be in your specific case.
I think destroyPersistentStoreAtURL method of NSPersistentStoreCoordinator is what you want.
It will remove the data store and journal files and all other things that need to be removed.
Look at Apple documentation.

Resources