Implementing Sharing with NSPersistentCloudKitContainer in SwiftUI - swift

Is it possible?
I've got a new iOS 14 app set up with Core Data and Cloudkit. I had to make a few changes to my Persistence.swift file to get it working but it's working without a hitch.
I'm interested in implementing sharing with other iCloud Users, but a lot of the documentation is out of date and confusing and it seems like it might not have been possible at one point but it is now?
I think the first step is to make my database shared? I'm adding the following line to my Persistence.swift file
container.persistentStoreDescriptions.first!.cloudKitContainerOptions?.databaseScope = .shared
(Here's the whole thing):
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Shopmatic")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.persistentStoreDescriptions.first!.cloudKitContainerOptions?.databaseScope = .shared
}
}
but when I run the app I get the following error
Thread 1: "CKDatabaseScopeShared is not supported with NSPersistentCloudKitContainer"
Which is not encouraging, but maybe it's possible to implement sharing with a public database scope?
I'm not really sure what the next steps are? Implementing my own NSPersistentContainer that both syncs to cloudkit and allows sharing?

I believe the answer is no based on this thread on the Apple dev forums (I'm MasonAndMuse there), but there's one Apple engineer in there who was saying yes but wouldn't elaborate. I stopped trying soon after, not finding any way around the opaque nature of the system as it is now. Nothing I've seen indicates someone else has gotten it working but I haven't looked very closely after giving up 8 months ago. Of course there might be a way - I'd love to be proven wrong!
I assume they introduce sharing in iOS 15, but that's not very helpful now... Too bad because it's so close now and it will save SO much time if/when they add it in...

The Apple engineer's answer in the forum reads quite clearly to me:
NSPersistantCloudKitContainer does not support sharing!
You need to implement CloudKit sharing through CK APIs.
I have not implemented such a feature yet, but I would approach it like this:
Get the NSPersistantCloudKitContainer implementation working.
Implement CloudKit sharing on the same container.
Start passing specific records from NSPersistantCloudKitContainer into the sharing feature implemented in step 2.
To access the shared Cloudkit database with CKDatabase.Scope = shared:
If this is the NSPersistentCloudKitContainer
:
container = NSPersistentCloudKitContainer(name: "AppName")
I would try to access the shared DB of this container like so
:
let myAppsCloudKitContainer = CKContainer(identifier: "iCloud.com.name.AppName")
let myContainersSharedDatabase = myAppsCloudKitContainer.database(with: .shared)
To access data from NSPersistantCloudKitContainer for use in the CloudKit sharing feature use NSPersistantCloudKitContainer instance method
record(for managedObjectID: NSManagedObjectID)
(as of macOS 11 / Xcode 12)

Related

I get a warning in my console when load data from Coredata

i have a small problem with my code. When I load data from my CoreData I get warnings in my consol but I don´t know why. I´m not a professional programmer and also google couldn´t really help me so now I´m here.
I load my data like this:
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var data: [DataToLoad] = []
override func viewDidLoad(){
do{
data = try context.fetch(DataToLoad.fetchRequest())
}catch{
print("error")
}}
But when I load it like that the console says...
"2020-09-05 20:32:04.884728+0200 Test-Application[3850:348569] [general] 'NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release"
...after I start the App.
So I mean everything is working fine, but where is my error that this massage appears.
I hope you can help me.
Sorry for my bad english I hope you can understand everything.
Have a nice day and thank you all.
The app is still working because it's a warning and not an error but it's good to not ignore warnings.
In your case, the warning occurs because NSKeyedUnarchiveFromData is deprecated. This means it's outdated and will be replaced in the future and the app might then stop working.
Kaira Diagne explains:
With iOS 12 Apple has started adopting NSSecureCoding across the entire platform. For Core Data this means that the default ValueTransformer, which uses NSCoding to transform a custom data type into a format that can be stored in the persistent store, at some point will change as well.
In your case NSSecureUnarchiveFromData instead of NSKeyedUnarchiveFromData in Core Data.
This means that the first thing we need to do is make sure that the data type of every transformable property in our data model conforms to secure coding.
(Link: NSSecureCoding and transformable properties in Core Data, 2020)
For your app to also work in the future you will have to dig somewhat deeper into the code and see where you are using the deprecated methods and classes and replace them.

Inter-App Data Migration (migrating data to new app version)

I currently have a Swift iOS app on Apple's App Store. I have many users and I would like to make a new version and help current users migrate to the new version. FYI: the new version is an Ionic app.
Data-wise, my app is using Core Data without any iCloud or sync support. It contains JSON data and also multiple images. So I'd need to bundle the current data and find a way of bringing it to the new ionic app version.
Basically my question is: Is there a way of writing in the app's documents directory and let the new version grab that file to import its data? Is there a way of letting both apps transmit data other than AirDrop or Custom URLs?
I don't want to upload the data remotely, I'd like to do this all locally on the device and also seamlessly so the user don't have to manually do anything.
Suggestions are welcome, thanks!
I would suggest using App Groups to get a shared container. I’m not familiar with Ionic, but this is quite straightforward in native Swift. It allows multiple apps or extensions to access a shared container of data, like the image below:
(Image from https://agostini.tech/2017/08/13/sharing-data-between-applications-and-extensions-using-app-groups/ )
This would require an update to the existing app to copy data to the shared container and then users would have to install the new app while the old one was still installed, because the shared container will be deleted when there are no installed apps using it.
It can be set up like this:
1: Enable App Groups in your project's Capabilities tab (for both apps).
2: Add a new app group and name it something like "group.appDomain.appName" or similar.
3: Now that the App Group is set up, it’s shared container can be used in several ways (User Defaults, NSCoding or Core Data).
For shared User Defaults:
let defaults = UserDefaults.init(suiteName: "group.appDomain.appName")
defaults.set("Example", forKey: "exampleKey")   
defaults.synchronize()
More info from Apple here.
For NSCoding:
let sharedContainerDirectory: URL = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.appDomain.appName")!
let sharedArchiveURL: URL = sharedContainerDirectory.appendingPathComponent("whateverYouNeed")
NSKeyedArchiver.archiveRootObject(yourObject, toFile: sharedArchiveURL.path)
For Core Data:
You can set up the container as below. I have taken this code from this answer as I have not actually tried this with Core Data myself.
You use containerURL(forSecurityApplicationGroupIdentifier: "group.appDomain.appName")! to make this work in shared container.
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application.
This implementation creates and returns a container, having loaded the store for the application to it.
This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "xx")
let appName: String = "xx"
var persistentStoreDescriptions: NSPersistentStoreDescription
let storeUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.appDomain.appName")!.appendingPathComponent("xx.sqlite")
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
description.url = storeUrl
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.xxx.xx.container")!.appendingPathComponent("xx.sqlite"))]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
This answer provides a way to migrate the persistent store.
As I mentioned, I am not familiar with Ionic, so I’m not sure how working in that context might change this technique.
I hope this is helpful.
I would have just left a comment but I am unable to do so.
I was able to see that after loading a native iOS application then a Ionic project with the same bundle structure that the data within Library/Application Support/DisplayName.sqlite was still there and the data within the database still intact. (Note: this is deploying using Xcode not through the App Store)
You can see this using Xcode -> Window -> Devices and Simulators -> Devices tab -> Click on your app -> settings cog -> Download container -> after saving Show package contents
I was unable to use the Ionic SQLite native plugin to open the database for some reason. That is as far as I could get. I think it might have something to do with the space of the Application Support folder.
You can do the transition without using AirDrop or Custom Url. The idea is based on how ionic works. Much of the ionic functionality depends upon the plugins developed by community like working with hardware features.
Dealing with device specific features are done in native codes then JS wrapper classes are created for making a bridge between your code and native code.
I would suggest you to write native code which will access the data and files from CoreData and then use the cordova plugin tech to setup communication between the native code and the ionic code. here is a good post on Creating Custom Plugin for ionic and a sample github project

Unknown Error when adding an CSSearchableItem to Core Spotlight (MacOS)

I recently wanted to power the search in one of my projects with Core Spotlight. However, whenever I add an CSSearchableItem to the SearchIndex, I get an error in the completion handler with the description:
The operation couldn’t be completed. (CSIndexErrorDomain error -1.)
According to Apple's reference, the error code -1 refers to Unknown Error, which isn't exactly helpful. I added both CoreSpotlight and CoreServices frameworks to my app and I really have no idea of what I might have done wrong.
I put together a minimal example:
import Foundation
import CoreSpotlight
print("Start indexing...")
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String)
attributeSet.title = "test element"
attributeSet.contentDescription = "This is a description."
attributeSet.keywords = ["test1", "test2", "test3"]
let item = CSSearchableItem(uniqueIdentifier: "123455", domainIdentifier: "TestDomain", attributeSet: attributeSet)
var ready = false
CSSearchableIndex.default().indexSearchableItems([item]) { (error) in
if error == nil {
print("Success")
} else {
print(error?.localizedDescription)
}
ready = true
}
//Wait for the block to finish
while (ready == false) {
sleep(1)
}
print("Finish indexing...")
I managed to compile Apple's example project for Core Spotlight from WWDC17, and it does in fact work without an error. However, I can't obtain the indexed items with the systemwide Spotlight search.
Does anyone have an idea what might be off? By the way, I'm running the latest High Sierra release.
[Edit] Just saw, that there is in fact someone else with this question. However, the question isn't answered yet: Error while using CoreSpotlight
[Edit2] After updating to 10.13.2, the behaviour changed. This piece of code put in a playground is working now; however, the very same code put in my app does still produce an error, it does contain more information though. Printing the error object results in:
Error Domain=CSIndexErrorDomain Code=-1003 "(null)"
UserInfo={NSUnderlyingError=0x60000105f6e0
{Error Domain=NSCocoaErrorDomain Code=4097
"Couldn’t communicate with a helper application."}}
As it displays to me, this is clearly a bug in the framework, or what do you think about that?
With macOS 10.13, SIP and the App Sandbox seem to have become a little more strict.
After a lot of digging to get this work, here's what I did, and should work for others reading this too:
In Xcode, toggle the "App Sandbox" capability for your target in which your CoreSpotlight code runs. If this is in a framework, it needs to be the hosting application's target. This shouldn't be necessary for new projects (for details, see this bug report).
Enable development signing for the application/binary which will run the CoreSpotlight code. In my case, it was the test host.
Eventually I managed to resolve the problem somehow, I followed the hint that the code was working in a playground. So I did what I wanted to do anyway and put the code in a framework.
Without any further changes, it started to work. So apparently it had to do with the project configuration but it would be great if somebody came up with an idea what's the underlying reason for Spotlight to fail.

Core Data: Where is SingleViewCoreData.sqlite being saved?

I am quite new to core data, which is why I am at the beginning of a learning process.
Today I tried to save Data from a SpriteKit Game in CoreData, which resulted in a bunch of errors.
Because the game template from XCode does not provide the use of core data, I had to import the Appdelegate Code (CoreData Stack) from a singleview Template. And this is where the problems begin - I guess.
I inspected the persistent store coordinator and I found:
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite")
First, I can not find the "SingleViewCoreData.sqlite" file in the project directory. So where is it stored? Or is it created automatically?
Second I got the "unexpectedly found nil while unwrapping an Optional value" Error. I think there might be a link to the first question?
Thanks so far. I hope you can understand my bad english.
Krusel

Is it Possible to put the whole CoreData-Stack in a framework?

I have a very large Project and I am right in a refactoring session. At the beginning my idea sounds good to me, but now i am stuck and confused and near before a git rollback. You guys are my last chance.
We’ve done a GUI application that shows some data that are living in CoreData. A background Process is fetching data from time to time from the network and put them into the CoreData.
So I’ve got the request thing -> pushing into CoreData and <- getting it out onto the GUI.
That works for very good for us. It’s straight forward and nothing special. My Company is programming this application since January and the requesting and data part gets huge. We also have a 100% test coverage! \o/. Working in that project could be so nice… if… if the GUI things could be separated.
As time goes by I have to split the dev-team into two parts: one for the gui (and the several gui applications that all based on the same data model) The second one for the Request-and-CoreDate part. My Model interface is clean enough to have a nice entry point, so i thought i would be easy to move the Request and CoreData sections into a library-project in my Workspace.
hm, well. no!
I put the Kit.xcdatamodeld into the new library project. I generate all models and add all the code from the main application into the lib. I resolved every problem and start to move the first basic tests into the new project.
I put the managedObjectModel part from our ApplicationDelegate into a DataController in the new project. And changed the first call to use this managedObject from the new class.
let dc = DataController()
_ = GlobalScope(context: dc.managedObjectContext!)
When I run the first test that should store a simple entity than an error occurs in managedObjectModel:
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource(" <LibName>Kit", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
fatal error: unexpectedly found nil while unwrapping an Optional value
My Question is: Is it possible to do the whole CoreData stuff into a Framework? I wan’t have any data model or requests in my gui applications? The plan ist to include the framework and use the getXY() setXY() functions of my models. Also the whole network background fetching should be places in the framework.
But it seams that there is no mainBundle in a framework. So how to get the database the right way?
Thank you very much.
ps
First, Core Data is already a framework. You are looking to put your layer on top of Core Data into a framework. Is it possible? Probably, but it is not a great idea.
There is no value in putting a third (or more) of your application into a framework. It will not make anything cleaner or easier. In fact it will make debugging of your application harder.
Separate out the code using groups in Xcode and call it there. Trying to put things into separate Xcode projects (which is what you would need) would lead to more issues than it is worth.
BTW, there is no way to compile the model into the framework as that is an independent file. Therefore your primary application would need to embed the model file anyway.
You could explore creating your own custom CocoaPod like structure but again, it really is not worth it and maintainability will decrease.
The problem is this line:
let modelURL = NSBundle.mainBundle().URLForResource(" <LibName>Kit", withExtension: "momd")!
It's looking in the main bundle for that resource, which doesn't exist. You'll want to use bundleWithIdentifier or bundleForClass instead, like:
let modelURL = NSBundle(identifier:"com.whatever.yourFrameworkIdentifier").URLForResource...