Core Data sharing with CloudKit: Unable to Accept Share - ios

I had my app working with Core Data, then CloudKit to sync between devices and now I'd like to share data between users. I watched both Build apps that share data through CloudKit and Core Data and What's new in CloudKit WWDC21 and thought that I got the concepts down. CloudKit uses zone sharing and CKShares to handle sharing and Core Data attaches to this implementation natively in iOS15.
I setup my Core Data stack as such:
/// Configure private store
guard let privateStoreDescription: NSPersistentStoreDescription = persistentContainer.persistentStoreDescriptions.first else {
Logger.model.error("Unable to get private Core Data persistent store description")
return
}
privateStoreDescription.url = inMemory ? URL(fileURLWithPath: "/dev/null") : privateStoreDescription.url?.appendingPathComponent("\(containerIdentifier).private.sqlite")
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
persistentContainer.persistentStoreDescriptions.append(privateStoreDescription)
/// Create shared store
let sharedStoreDescription: NSPersistentStoreDescription = privateStoreDescription.copy() as! NSPersistentStoreDescription
sharedStoreDescription.url = sharedStoreDescription.url?.appendingPathComponent("\(containerIdentifier).shared.sqlite")
let sharedStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: containerIdentifier)
sharedStoreOptions.databaseScope = .shared
sharedStoreDescription.cloudKitContainerOptions = sharedStoreOptions
persistentContainer.persistentStoreDescriptions.append(sharedStoreDescription)
persistentContainer.loadPersistentStores(...)
Implemented the SceneDelegate user acceptance:
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
let container = PersistenceController.shared.persistentContainer
let sharedStore = container.persistentStoreCoordinator.persistentStores.first!
container.acceptShareInvitations(from: [cloudKitShareMetadata], into: sharedStore, completion: nil) //TODO: Log completion
}
However after sharing the NSObject as such in my UI using UICloudSharingController as seen below:
let object: NSObject = // Get Object from view context
let container = PersistenceController.shared.persistentContainer
let cloudSharingController = UICloudSharingController { (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
container.share([object], to: nil) { objectIDs, share, container, error in
completion(share, container, error)
Logger.viewModel.debug("Shared \(household.getName())")
}
}
cloudSharingController.delegate = self
self.present(cloudSharingController, animated: true) {}
My SceneDelegate method is never called and I get the following alert when I press the invite from the messages app. I'm not quite sure what is wrong in this case as on the CloudKit developer console I see the object in a private database with the zone of com.apple.coredata.cloudkit.share.[UUID]. I have not released the app yet so I'm not sure where it is getting version information from as both apps were launched from the Xcode debugger(same version & build). Additionally I was unable to find reference this alert on other questions so any advice, suggestions, or help is welcome as I have been stuck on this for a few evenings. Please let me know if there is more information that could shine light on this problem.

I had the same problem and it was solved when I added the CKSharingSupported key with a Bool value of true in the Info.plist
After that I was able to share with no problem.

Related

Using CoreData as dynamic Widget Intents in SwiftUI

I've followed along the Apple Developer Code-Along videos (https://developer.apple.com/news/?id=yv6so7ie) as well as looked at this repo with different Widget types (https://github.com/pawello2222/WidgetExamples) in search on how to populate a dynamic intent with items from Core Data.
My question is: How can I used the saved Core Data objects from the user to have them as "filters" or options in widget settings?
My Core Data model is called Favourite and it is a Class Definition CodeGen file.
I have added the Intent target to my project and can get it to appear in the Widget settings, but when I tap in to "Choose" the list is empty. However, in my Core Data there are 3 saved items.
I have tried doing simple things like print(CoreDM.shared.getAllFavourites()) to see if I am even retrieving them all, but not listing in the settings, but the console prints out:
<NextDeparturesIntent: 0x283490bd0> {
favourite = <null>;
}
At this point I'm stuck on understanding on how I can get my Favourites visible and then usable. It seems everything else is hooked up and working or ready but the retrieval.
I have also tried re-adding into the Info.plist of the intent the IntentSupported of the intent's name: NextDeparturesIntentHandling:but that had no success.
Files
Core Data Model - Favourite - FavouritesCDModel
In my Core Data model I have more options but for this example:
UUID
beginName
finishName
Widget Intent - NextDepartures.intentdefinition
This is what has been set up as followed by Apple's Code Along videos:
app-name-intent - IntentHandler.swift
import Intents
class IntentHandler: INExtension, NextDeparturesIntentHandling {
let coreDM = CoreDataManager.shared
func provideFavouriteOptionsCollection(
for intent: NextDeparturesIntent,
with completion: #escaping (INObjectCollection<FavouriteRoutes>?, Error?) -> Void
) {
dump( coreDM.getAllFavourites() ) // <--- debug line
let favouriteRoutes = coreDM.getAllFavourites().map {
FavouriteRoutes(identifier: $0.uniqueId, display: $0.departingStopName!)
}
let collection = INObjectCollection(items: favouriteRoutes)
completion(collection, nil)
}
override func handler(for intent: INIntent) -> Any {
return self
}
}
CoreDataManager
import CoreData
final class CoreDataManager {
static let shared = CoreDataManager()
private init() {}
private let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FavouritesCDModel")
container.loadPersistentStores(completionHandler: { description, error in
if let error = error as NSError? {
fatalError("Core Data Store failed \(error.localizedDescription)")
}
})
return container
}()
var managedObjectContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func getAllFavourites() -> [Favourite] {
let fetchRequest: NSFetchRequest<Favourite> = Favourite.fetchRequest()
do {
return try managedObjectContext.fetch(fetchRequest)
} catch {
return []
}
}
}
I didn't realise that the app, widget, and other targets are all sandboxed.
I incorrectly assumed everything within the same app ecosystem would be allowed access to the same items.
In order to get the above code to work is adding the file to the App Groups and FileManager.
CoreDataManager
Inside the persistentContainer add in the storeURL and descriptions:
let storeURL = FileManager.appGroupContainerURL.appendingPathComponent("COREDATAFILE.sqlite")
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
FileManager+Ext
Create a FileManager extension for the container url:
extension FileManager {
static let appGroupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.domain.app")!
}
Info.plist
Make sure that the Info.plist files have access to the app group
Signing and Capabilities
Make sure you add the App Groups capability to each target that needs it, and add it in App Store Connect

User Default Values Changing to Previous Values after some time - Swift

Please don't mark my question duplicate.
Gone through the following links and tried what they mentioned but no luck.
UserDefaults in IOS 10 is sometimes showing old value
User Default Values Changing to Previous Values Seemingly Randomly - Swift
UserDefaults in IOS 10 is sometimes showing old value
When a user login into the app I am storing some values from login API response in user defaults.
UserDefaults.standard.set(val, forKey: "XYZ")
When the user log out of the app I'm deleting user defaults.
logOutAlert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { (action: UIAlertAction!) in
// Mark:- Function that remove user defaults data
self.resetDefaults()
BackgroundTask().stopUpdate()
let loginVC = self.storyboard?.instantiateViewController(withIdentifier: "LoginVC") as! LoginVC
self.showAlert(title: Constant.projectTitle, message: "Successfully loged out.")
let appDel:AppDelegate = UIApplication.shared.delegate as! AppDelegate
appDel.window?.rootViewController = loginVC
}))
// Reset userdefaults
func resetDefaults() {
//let domain = Bundle.main.bundleIdentifier!
//UserDefaults.standard.removePersistentDomain(forName: domain)
UserDefaults.standard.dictionaryRepresentation().keys.forEach(UserDefaults.standard.removeObject(forKey:))
// let defaults = UserDefaults.standard
// let dictionary = defaults.dictionaryRepresentation()
// dictionary.keys.forEach { key in
// defaults.removeObject(forKey: key)
// }
}
Commented lines are the ways I tried to solve a problem.
Please go through following scenario
Install app
Log in (user A)
Log out (user A)
Log in (user B)
Kill app from memory (not uninstall)
Wait for around 20 mins.
Relaunch app.
Issue - user A's user defaults data restored automatically.
Thanks in advance. Any help surely appreciated, sorry for my English.
Try this maybe it could work .. Use this removeobject on the logout button Action method .
let defaults = UserDefaults.standard
defaults.synchronize()
UserDefaults.standard.removeObject(forKey: "email")
UserDefaults.standard.removeObject(forKey: "name")
UserDefaults.standard.removeObject(forKey: "userid")
UserDefaults.standard.removeObject(forKey: "mobno")
UserDefaults.standard.removeObject(forKey: "profileimage")
UserDefaults.standard.removeObject(forKey: "iphoneid")
You might want to try another way around:
//Set an empty dictionary for the main domain, instead of removing the old one
guard let domain = Bundle.main.bundleIdentifier else { return }
let emptyDomain = [String : Any]()
UserDefaults.standard.setPersistentDomain(emptyDomain, forName: domain)
I have a theory that the removePersistentDomainForName method is buggy, and it deletes the old domain, but fails to instantiate an empty domain dictionary after the deletion, so it just keeps the old one for that case.
I think the problem is that you're trying to remove all keys from UserDefaults.standard without any filter. UserDefaults.standard contains more keys than you think and some of them are managed by iOS itself.
In order to identify the keys you manage you should put a prefix or (example: "MYAPP_username") and remove only these keys.
Here is the code:
let keys = UserDefaults.standard.dictionaryRepresentation().keys.filter { return $0.starts(with: "MYAPP_") }
for key in keys {
UserDefaults.standard.removeObject(forKey: key)
}
This should work and hope it does ;-)
I think this solution would be helpful in your situation. This will clean all user userdefaults and give you something like the app is just installed.
I would recommend storing all user related sensitive data in keychain.
Try
NSUserDefaults.standardUserDefaults().removePersistentDomainForName(NSBundle.mainBundle().bundleIdentifier!)
NSUserDefaults.standardUserDefaults().synchronize()
It is best to add synchronize if your target version is iOS 11 or lower, which will help to write to disc immediately.
Add Observer For changes in Defaults
Debug if you are actually changing from your code unintensionaly using below approach.
In your AppDelegate Add below code:
UserDefaults.standard.addObserver(self, forKeyPath: "XYZ", options: NSKeyValueObservingOptions.new, context: nil)
And observe using method
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
// Check when It's getting trigged.
}
Apple's Synchronize method
If you are working this Asynchronous tasks you may have to try calling this method UserDefaults.standard.synchronize().
Check what value it returns. If it is false, (I have never seen this method returning false) You can conclude something internal things are blocking you from saving to disk.
func synchronize() -> Bool
Return value
true if the data was saved successfully to disk, otherwise false.

Converting local Realm to synced Realm in the middle of app life cycle (in Swift)

My app will have a paid feature called multi-devices sync. I would like to implement the feature with Realm Cloud - Query Based Sync.
I know how to convert local Realm to synced Realm thanks to
this thread.
But this is based on the scenario that users sync their Realm from the app start - before opening their non-synced local realm. That doesn’t work for me because my users will start sync when they paid for it.
Therefore, I have to convert their local Realm in the middle of app life cycle and the local Realm is already opened by that time.
My issue comes in here. When I try to convert local realm to synced realm, app crashes with this message:
Realm at path ‘…’ already opened with different read permissions.
I tried to find a way to close local Realm before converting it, but Realm cocoa does not allow me to close a Realm programmatically.
Here’s my code converting local Realm to synced Realm.
func copyLocalRealmToSyncedRealm(user: RLMSyncUser) {
let localConfig = RLMRealmConfiguration()
localConfig.fileURL = Realm.Configuration.defaultConfiguration.fileURL
localConfig.dynamic = true
localConfig.readOnly = true
// crashes here
let localRealm = try! RLMRealm(configuration: localConfig)
let syncConfig = RLMRealmConfiguration()
syncConfig.syncConfiguration = RLMSyncConfiguration(user: user,
realmURL: realmURL,
isPartial: true,
urlPrefix: nil,
stopPolicy: .liveIndefinitely,
enableSSLValidation: true,
certificatePath: nil)
syncConfig.customSchema = localRealm.schema
let syncRealm = try! RLMRealm(configuration: syncConfig)
syncRealm.schema = syncConfig.customSchema!
try! syncRealm.transaction {
let objectSchema = syncConfig.customSchema!.objectSchema
for schema in objectSchema {
let allObjects = localRealm.allObjects(schema.className)
for i in 0..<allObjects.count {
let object = allObjects[i]
RLMCreateObjectInRealmWithValue(syncRealm, schema.className, object, true)
}
}
}
}
Any help will be appreciated.
Thanks.
I made a copy of the local realm file and opened the copy with RLMRealmConfiguration. Afterwards, just delete both files. It is not the best solution, but it works

Transfer data from project to widget in swift

At my project i need to send user id's to widget in iOS. But for do that, my user needs to open application once. Without opening, information stays only 1 day, after that it vanishes and widget stops showing information and await for opening application.
For do that i used appGroup.
What is the correct way to use transfer data from my project to widget?
Swift 5
Follow these steps to pass data from the host app to extensions.
Select project target > Capabilities > add new app group (if you have enabled permissions for your developer account otherwise enable that first)
Select the extension target and repeat the same.
if let userDefaults = UserDefaults(suiteName: "group.com.yourAppgroup") {
createEventDic.removeAll()
let eventDic = NSMutableDictionary()
eventDic.setValue("YourString", forKey: "timeFontName")
createEventDic.append(eventDic)
let resultDic = try? NSKeyedArchiver.archivedData(withRootObject: createEventDic, requiringSecureCoding: false)
userDefaults.set(resultDic, forKey: "setWidget")
userDefaults.synchronize()
} else {
}
Now go to your app extension and do these steps to get the passed data.
if let userDefaults = UserDefaults(suiteName: "group.com.yourAppGroup") {
guard let testcreateEvent = userDefaults.object(forKey: "testcreateEvent") as? NSData else {
print("Data not found in UserDefaults")
return
}
do {
guard let eventsDicArray = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(testcreateEvent as Data) as? [NSMutableDictionary] else {
fatalError("loadWidgetDataArray - Can't get Array")
}
for eventDic in eventsDicArray {
let timeFontName = eventDic.object(forKey: "timeFontName") as? String ?? ""
}
} catch {
fatalError("loadWidgetDataArray - Can't encode data: \(error)")
}
}
Hopefully, it will help. Cheers!
For do that i used appGroup.
What is the correct way to use transfer data from my project to
widget?
What you did so far (App Grouping) is one of the steps that you should follow. Next, as mentioned in App Extension Programming Guide - Sharing Data with Your Containing App:
After you enable app groups, an app extension and its containing app
can both use the NSUserDefaults API to share access to user
preferences. To enable this sharing, use the initWithSuiteName: method
to instantiate a new NSUserDefaults object, passing in the identifier
of the shared group.
So, what you have to do so far is to let the data to be transferred by the UserDefautls. For instance:
if let userDefaults = UserDefaults(suiteName: "group.com.example.myapp") {
userDefaults.set(true, forKey: "myFlag")
}
thus you could pass it to the widget:
if let userDefaults = UserDefaults(suiteName: "group.com.example.myapp") {
let myFlag = userDefaults.bool(forKey: "myFlag")
}
And you follow the same approach for passing the data vise-versa (from the widget to the project).
In Xamarin Forms, we need to use DI to pass the data to the ios project then we can put it into NSUserDefaults
Info: Grouping application is mandatory
Xamarin iOS project - Putting Data into NSUserDefaults
var plist = new NSUserDefaults("group.com.test.poc", NSUserDefaultsType.SuiteName);
plist.SetBool(true, "isEnabled");
plist.Synchronize();
Today Extension - Getting data from NSUserDefaults
var plist = new NSUserDefaults("group.com.test.poc", NSUserDefaultsType.SuiteName);
var result = plist.BoolForKey("isEnabled");
Console.WriteLine($"The result of NSUserdefaults: logesh {result}");

Cannot share CloudKit CKShare record

I'm trying to implement cloud kit sharing in my application, however, whenever I try to share an item using a UICloudSharingController I'm getting a consistent error:
I am presented with the initial share popover for adding people, and then when I select one of the options on how I'd like to send the invitation (i.e: by mail), the UICloudSharingControllerDelegate returns calling:
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error)
And throws the error:
CKError 0x170245d60: "Invalid Arguments" (12); "An added share is being saved without its rootRecord (CKRecordID: 0x1700343e0; recordName=C9FA0E96-3461-4C9E-AB99-3B342A37A07A, zoneID=PrivateDatabase:__defaultOwner_)"
I've already created a custom zone in the private cloud database for the user whose zoneId is "PrivateDatabase". I've created an object and successfully saved it to iCloud and it is linked to the custom zone I previously created. The code I am using to present the UICloudSharingController is as follows:
let object = // A core data representation of a CKRecord //
let share = CKShare(rootRecord: object.record) //record is a CKRecord that is stored with the core data object
share[CKShareTitleKey] = object.name as? CKRecordValue
share[CKShareThumbnailImageDataKey] = UIImagePNGRepresentation(object.categoryKey.icon()) as? CKRecordValue
share[CKShareTypeKey] = "reverse.domain" as CKRecordValue
share.publicPermission = .readOnly
let sharingController = UICloudSharingController(share: share, container: self.container)
sharingController.delegate = self
sharingController.availablePermissions = [.allowPrivate, .allowReadOnly]
sharingController.popoverPresentationController?.sourceView = sourceView
controller.present(sharingController, animated: true, completion: nil)
What am I missing here?
You are using the wrong initializer for an instance of UICloudSharingController.
There are two different initializers for two different use cases.
If an item is shared the first time (not already shared) you have to create a new instance of CKShare with a rootRecord (as you did in your code) but then you have to initialize your UICloudSharingController with this Initializer init(preparationHandler:)
If an item is already in a sharing process, then you have to fetch the existing share from iCloud and initialize your UICloudSharingController with this Initializer init(share:container:)
So, the error in your case is, that you create a new instance of CKShare but then use the wrong initializer.
more from Apple
did you check in iCloud, whether the share has the correct link to the root record in the correct zone etc., and/or the rootRecord is existing...

Resources