UICloudSharingController error: recordChangeTag specified, but record not found - swift

I'm using NSPersistentCloudKitContainer for storing data and UICloudSharingController for sharing.
Here's how UICloudSharingController is created (that's pretty basic stuff):
func share(_ record: Record, avatar: UIImage) {
let controller = shareControllerForRecord(record, avatar: avatar)
controller.delegate = self
controller.popoverPresentationController?.sourceView = self.view
controller.popoverPresentationController?.permittedArrowDirections = []
present(controller, animated: true)
}
func shareControllerForRecord(_ record: Record, avatar: UIImage) -> UICloudSharingController {
if let share = share(for: record) {
let controller = UICloudSharingController(share: share, container: self.cloudKitContainer)
return controller
} else {
let controller = UICloudSharingController { [weak self] (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
guard let self = self else { return }
self.persistentContainer.share([record], to: nil) { objectIDs, share, container, error in
if let share = share {
record.managedObjectContext?.performAndWait {
share[CKShare.SystemFieldKey.title] = record.title
share[CKShare.SystemFieldKey.thumbnailImageData] = avatar.pngData()
}
}
completion(share, container, error)
}
}
return controller
}
}
Once UICloudSharingController is created the user do basic stuff, and in most cases sharing works fine. But some users (I can see it in analytics) get this error:
Error Domain=_UIShareErrorDomain Code=0 "(null)" UserInfo={NSUnderlyingError=0x28399e340 {Error Domain=CKErrorDomain Code=2 "CKInternalErrorDomain: 1011" UserInfo={ContainerID=iCloud.net.bundleidentifier, NSDebugDescription=CKInternalErrorDomain: 1011, CKPartialErrors={ "<CKRecordID: 0x2837f2280; recordName=cloudkit.zoneshare, zoneID=com.apple.coredata.cloudkit.share.9A2156CD-2E69-4987-966E-AC5A3F593737:defaultOwner>" = "<CKError 0x28399d890: "Unknown Item" (11/2003); server message = "recordChangeTag specified, but record not found"; op = CE76405043B1BE49; uuid = C567647F-331B-4002-9893-2791F38131B3; container ID = "iCloud.net.bundleidentifier">"; }, RequestUUID=C567647F-331B-4002-9893-2791F38131B3, NSLocalizedDescription=Failed to modify some records, CKErrorDescription=Failed to modify some records, NSUnderlyingError=0x28399e430 {Error Domain=CKInternalErrorDomain Code=1011 "Failed to modify some records"
For me it seems like remote database doesn't have the Record uploaded yet. But it's strange, because these are not newly created records. Besides, for majority of users sharing works.
Can anyone advise?

Related

CKShare - Failed to modify some records error - CloudKit

I'm trying to share a record with other users in CloudKit but I keep getting an error. When I tap one of the items/records on the table I'm presented with the UICloudSharingController and I can see the iMessage app icon, but when I tap on it I get an error and the UICloudSharingController disappears, the funny thing is that even after the error I can still continue using the app.
Here is what I have.
Code
var items = [CKRecord]()
var itemName: String?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.row]
let share = CKShare(rootRecord: item)
if let itemName = item.object(forKey: "name") as? String {
self.itemName = item.object(forKey: "name") as? String
share[CKShareTitleKey] = "Sharing \(itemName)" as CKRecordValue?
} else {
share[CKShareTitleKey] = "" as CKRecordValue?
self.itemName = "item"
}
share[CKShareTypeKey] = "bundle.Identifier.Here" as CKRecordValue
prepareToShare(share: share, record: item)
}
private func prepareToShare(share: CKShare, record: CKRecord){
let sharingViewController = UICloudSharingController(preparationHandler: {(UICloudSharingController, handler: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
let modRecordsList = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modRecordsList.modifyRecordsCompletionBlock = {
(record, recordID, error) in
handler(share, CKContainer.default(), error)
}
CKContainer.default().privateCloudDatabase.add(modRecordsList)
})
sharingViewController.delegate = self
sharingViewController.availablePermissions = [.allowPrivate]
self.navigationController?.present(sharingViewController, animated:true, completion:nil)
}
// Delegate Methods:
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
print("saved successfully")
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("failed to save: \(error.localizedDescription)")// the error is generated in this method
}
func itemThumbnailData(for csc: UICloudSharingController) -> Data? {
return nil //You can set a hero image in your share sheet. Nil uses the default.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return self.itemName
}
ERROR
Failed to modify some records
Here is what I see...
Any idea what could be wrong?
EDIT:
By the way, the error is generated in the cloudSharingController failedToSaveShareWithError method.
Looks like you're trying to share in the default zone which isn't allowed. From the docs here
Sharing is only supported in zones with the
CKRecordZoneCapabilitySharing capability. The default zone does not
support sharing.
So you should set up a custom zone in your private database, and save your share and records there.
Possibly it is from the way you're trying to instantiate the UICloudSharingController? I cribbed my directly from the docs and it works:
let cloudSharingController = UICloudSharingController { [weak self] (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
guard let `self` = self else {
return
}
self.share(rootRecord: rootRecord, completion: completion)
}
If that's not the problem it's something with either one or both of the records themselves. If you upload the record without trying to share it, does it work?
EDIT TO ADD:
What is the CKShareTypeKey? I don't use that in my app. Also I set my system fields differently:
share?[CKShare.SystemFieldKey.title] = "Something"
Try to add this to your info.plist
<key>CKSharingSupported</key>
<true/>

How to check if email already exist before creating an account (Swift)

I know different variations of this question have been asked. However I seem to keep running into the same issue every time.
I want to check if an email already exist before the user pushes onto the next view. I will enter an email that exist in the database and the performSegue func is always called and pushes the user as if that email does not exist.
The only way I can check officially is when the user reaches the final sign up VC and the Auth.auth().createUser(withEmail: email as! String, password: password as! String ) { (user, error) in code will check for all errors.
However for good user experience I would hate for the user to have to click back three times to change the email address. Here is the code I have for the enter email view controller.
// Check if email is already taken
Auth.auth().fetchSignInMethods(forEmail: emailTextField.text!, completion: { (forEmail, error) in
// stop activity indicator
self.nextButton.setTitle("Continue", for: .normal)
self.activityIndicator.stopAnimating()
if let error = error {
print("Email Error: \(error.localizedDescription)")
print(error._code)
self.handleError(error)
return
} else {
print("Email is good")
self.performSegue(withIdentifier: "goToCreateUsernameVC", sender: self)
}
})
First off am I even entering the create property in the forEmail section? I added emailTextField.text because its the only way I know how even get the email the user typed. Does anyone know a better way I can do this?
How I create user accounts
This is an example of what I use. When a user provides credentials, FirebaseAuth checks if these credentials can be used to make a user account. The function returns two values, a boolean indicating whether the creation was successful, and an optional error, which is returned when the creation is unsuccessful. If the boolean returns true, we simply push to the next view controller. Otherwise, we present the error.
func createUserAcct(completion: #escaping (Bool, Error?) -> Void) {
//Try to create an account with the given credentials
Auth.auth().createUser(withEmail: emailTextField.text!, password: passwordConfirmTextField.text!) { (user, error) in
if error == nil {
//If the account is created without an error, then we will make a ProfileChangeRequest, i.e. update the user's photo and display name.
if let firebaseUser = Auth.auth().currentUser {
let changeRequest = firebaseUser.createProfileChangeRequest()
//If you have a URL for FirebaseStorage where the user has uploaded a profile picture, you'll pass the url here
changeRequest.photoURL = URL(string: "nil")
changeRequest.displayName = self.nameTextField.text!
changeRequest.commitChanges { error in
if let error = error {
// An error happened.
completion(false, error)
} else {
//If the change is committed successfully, then I create an object from the credentials. I store this object both on the FirebaseDatabase (so it is accessible by other users) and in my user defaults (so that the user doesn't have to remotely grab their own info
//Create the object
let userData = ["email" : self.emailTextField.text!,"name": self.nameTextField.text!] as [String : Any]
//Store the object in FirebaseDatabase
Database.database().reference().child("Users").child(firebaseUser.uid).updateChildvalues(userData)
//Store the object as data in my user defaults
let data = NSKeyedArchiver.archivedData(withRootObject: userData)
UserDefaults.standard.set(data, forKey: "UserData")
UserDefaults.standard.set([Data](), forKey: "UserPhotos")
completion(true, nil)
}
}
}
} else {
// An error happened.
completion(false, error)
}
}
}
Here is an example of how I would use it. We can use the success boolean returned to determine if we should push to the next view controller, or present an error alert to the user.
createUserAcct { success, error in
//Handle the success
if success {
//Instantiate nextViewController
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let nextVC = storyboard.instantiateViewController(withIdentifier: "NextVC") as! NextViewController
//Push typeSelectVC
self.navigationController!.pushViewController(viewController: nextVC, animated: true, completion: {
//We are no longer doing asynchronous work, so we hide our activity indicator
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
})
} else {
//We now handle the error
//We are no longer doing asynchronous work, so we hide our activity indicator
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
//Create a UIAlertController with the error received as the message (ex. "A user with this email already exists.")
let alertController = UIAlertController(title: "Error", message: error!.localizedDescription, style: .alert)
let ok = UIAlertAction(title: "OK", style: .cancel, action: nil)
//Present the UIAlertController
alertController.addAction(ok)
self.present(alertController, animated: true, completion: nil)
}
}
Let me know if this all makes sense, I know there is a lot to it. I'm just considering things you'll maybe find you need done anyways that you may not be aware of (like making change requests, or storing a data object on FirebaseDatabase).
Now for checking if the email is already taken:
Remember when I said that I post a user object to FirebaseDatabase upon account creation? Well we can query for the given email to see if it already exists. If it doesn't we continue with the flow as normal, without having actually created the account. Otherwise, we simply tell the user to pick another email address.
Pushing a user object to your database (taken from the above code):
if let firebaseUser = Auth.auth().currentUser {
//Create the object
let userData = ["email" : self.emailTextField.text!,"name": self.nameTextField.text!] as [String : Any]
//Store the object in FirebaseDatabase
Database.database().reference().child("Users").child(firebaseUser.uid).updateChildvalues(userData)
}
And now querying to see if somebody already has that email:
func checkIfEmailExists(email: String, completion: #escaping (Bool) -> Void ) {
Database.database().reference().child("Users").queryOrdered(byChild: "email").queryEqual(toValue: email).observeSingleEvent(of: .value, with: {(snapshot: DataSnapshot) in
if let result = snapshot.value as? [String:[String:Any]] {
completion(true)
} else {
completion(false)
}
}
}
Then we can call this like so:
checkIfEmailExists(email: emailTextField.text!, completion: {(exists) in
if exists {
//Present error that the email is already used
} else {
//Segue to next view controller
}
})

Singleton not accessible in Sirikit IntentHandler

I have a project that I want to add sirikit to. I added the intent and wanted to store values in my datastorage which is realm, when I tried to access the function that is used to create this task , I get an eeror. this is my code below
extension IntentHandler : INCreateTaskListIntentHandling {
public func handle(intent: INCreateTaskListIntent,
completion: #escaping (INCreateTaskListIntentResponse) -> Swift.Void) {
guard let title = intent.title else {
completion(INCreateTaskListIntentResponse(code: .failure, userActivity: nil))
return
}
CategoryFunctions.instance.createList(name: title.spokenPhrase,.....)
var tasks: [INTask] = []
if let taskTitles = intent.taskTitles {
let taskTitlesStrings = taskTitles.map {
taskTitle -> String in
return taskTitle.spokenPhrase
}
tasks = createTasks(fromTitles: taskTitlesStrings)
CategoryFunctions.instance.add(tasks: taskTitlesStrings, toList: title.spokenPhrase)
}
let response = INCreateTaskListIntentResponse(code: .success, userActivity: nil)
response.createdTaskList = INTaskList(title: title,
tasks: tasks,
groupName: nil,
createdDateComponents: nil,
modifiedDateComponents: nil,
identifier: nil)
completion(response)
}
}
this singlton instantiation works well in my app but I do not know why I get an error saying Use of unresolved identifier 'CategoryFunctions'
my CategoryFunctions singleton
class CategoryFunctions {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
var database:Realm!
static let instance = CategoryFunctions()
.....
...
Select your file in xcode, on the right, choose the File Inspector, then under Target Membership, pick your Intent.

iCloud Drive Issue: "[DocumentManager] Failed to associate thumbnails for picked URL"

I've created a JSON string file from an object containing multiple properties. This is the object:
RecipeFile : Codable {
var name: String
var theRecipeIngredients: [String]
var theRecipeSteps: [String]
var theRecipeRating: Int
var theRecipeCategory: String
var theRecipeIndexStrings: String
var theRecipeImage: String?
I create the JSON string file with this code:
let json_encoder = JSONEncoder()
let recipeFileName = recipeToDisplay.name! + UUID().uuidString + ".json"
let exportFilePath = getDocumentsDirectory().appendingPathComponent(recipeFileName)
do {
let jsonData = try json_encoder.encode(exportRecipeFile)
if let jsonString = String(data: jsonData, encoding: .utf8)
{
try jsonString.write(to: exportFilePath, atomically: false, encoding: .utf8)
}
} catch {
print(error.localizedDescription)
}
I upload it to iCloud Drive. I import the string file from iCloud Drive using UIDocumentPickerViewController. I can parse the imported file just fine. However, I get this message (edited to remove some path info) in the xCode debug area when func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) is called:
[DocumentManager] Failed to associate thumbnails for picked URL
file:///....Bourbon%20Chocolate%20Walnut%20Pie18D20181-DAFD-499C-9873-7D3E0794C37A.json
with the Inbox copy
file:///....Bourbon%20Chocolate%20Walnut%20Pie18D20181-DAFD-499C-9873-7D3E0794C37A.json:
Error Domain=QLThumbnail Code=2 "(null)"
UserInfo={NSUnderlyingError=0x149a042b0 {Error
Domain=GSLibraryErrorDomain Code=3 "Generation not found"
UserInfo={NSDescription=Generation not found}}}
Any idea what is causing this to be generated?
The didPickDocumentsAt code starts as follows:
let data = try? Data(contentsOf: urls[0]) as Data
let json_decoder = JSONDecoder()
do {
let importRecipeFile = try json_decoder.decode(RecipeFile.self, from: data!)
let importedRecipeToSave = Recipe(context: theMOC)
importedRecipeToSave.name = importRecipeFile.name
importedRecipeToSave.category = importRecipeFile.theRecipeCategory
importedRecipeToSave.rating = Int16(importRecipeFile.theRecipeRating)
importedRecipeToSave.terms = importRecipeFile.theRecipeIndexStrings
importedRecipeToSave.addedToGroceryList = false
You can safely ignore this message. When you import a file from iCloud, iOS tries to copy the thumbnail from iCloud to the imported copy, but for JSON files there's no thumbnail to copy and it logs this. This is not an error on your side.
I can reproduce this issue with SwiftUI with iOS 14. After Originally I present the UIDocumentPickerViewController after a ToolbarItem pressed and it works fine. Later I refactor the UI and the View is pushed from other parent View and this error occurs and the JSON is not received.
The same thing has happened to me, and I have not found a concrete solution in any forum. But, by testing and mixing code that I found in the forums finally worked for me. Still, I don't know exactly what's wrong.
I leave my code here in case it's useful for someone, although it's been many months since this question.
func importarCsv(sender: UIBarButtonItem) {
let types = [kUTTypePDF,kUTTypeUTF8PlainText]
let importMenu = UIDocumentPickerViewController(documentTypes: types as [String], in: .import)
if #available(iOS 11.0, *) {
importMenu.allowsMultipleSelection = false
}
importMenu.delegate = self
present(importMenu, animated: true, completion: nil)
}
extension MaterialViewController: UIDocumentPickerDelegate {
internal func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt urls: URL) {
print("urls : \(urls)")
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
print("close")
controller.dismiss(animated: true, completion: nil)
}
}
I had this problem when i presented the UIDocumentPicker from another UIViewController before adding it as a child view controller on its parent view controller.

How to update data on cloudKit without creating a new record?

Here is what I am trying to do. I have a simple journaling app with two views: a tableView that lists the titles of the entries and a viewController that has a text field for a title, and a textView for the text body (and a save button to save to cloudKit). On the viewController, I hit save and the record is saved to cloudKit and also added to the tableView successfully. This is all good.
I want to be able to edit/update the journal entry. But when I go back into the journal entry, change it in any way, then hit save again, the app returns to the tableView controller with an updated entry, but cloudKit creates a NEW entry separate from the one I wanted to edit. Then when I reload the app, my fetchRecords function fetches any extra records cloudKit has created.
Question: How do I edit/update an existing journal entry without creating a new entry in cloudKit?
Let me know if you need something else to further clarify my question.
Thanks!
Here are my cloudKit functions:
import Foundation
import CloudKit
class CloudKitManager {
let privateDB = CKContainer.default().publicCloudDatabase //Since this is a journaling app, we'll make it private.
func fetchRecordsWith(type: String, completion: #escaping ((_ records: [CKRecord]?, _ error: Error?) -> Void)) {
let predicate = NSPredicate(value: true) // Like saying I want everything returned to me with the recordType: type. This isn't a good idea if you have a massive app like instagram because you don't want all posts ever made to be loaded, just some from that day and from your friends or something.
let query = CKQuery(recordType: type, predicate: predicate)
privateDB.perform(query, inZoneWith: nil, completionHandler: completion) //Allows us to handle the completion in the EntryController to maintain proper MVC.
}
func save(records: [CKRecord], perRecordCompletion: ((_ record: CKRecord?, _ error: Error?) -> Void)?, completion: ((_ records: [CKRecord]?, _ error: Error?) -> Void)?) {
modify(records: records, perRecordCompletion: perRecordCompletion, completion: completion )
}
func modify(records: [CKRecord], perRecordCompletion: ((_ record: CKRecord?, _ error: Error?) -> Void)?, completion: ((_ records: [CKRecord]?, _ error: Error?) -> Void)?) {
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.savePolicy = .ifServerRecordUnchanged //This is what updates certain changes within a record.
operation.queuePriority = .high
operation.qualityOfService = .userInteractive
operation.perRecordCompletionBlock = perRecordCompletion
operation.modifyRecordsCompletionBlock = { (records, _, error) in
completion?(records, error)
}
privateDB.add(operation) //This is what actually saves your data to the database on cloudkit. When there is an operation, you need to add it.
}
}
This is my model controller where my cloudKit functions are being used:
import Foundation
import CloudKit
let entriesWereSetNotification = Notification.Name("entriesWereSet")
class EntryController {
private static let EntriesKey = "entries"
static let shared = EntryController()
let cloudKitManager = CloudKitManager()
init() {
loadFromPersistentStorage()
}
func addEntryWith(title: String, text: String) {
let entry = Entry(title: title, text: text)
entries.append(entry)
saveToPersistentStorage()
}
func remove(entry: Entry) {
if let entryIndex = entries.index(of: entry) {
entries.remove(at: entryIndex)
}
saveToPersistentStorage()
}
func update(entry: Entry, with title: String, text: String) {
entry.title = title
entry.text = text
saveToPersistentStorage()
}
// MARK: Private
private func loadFromPersistentStorage() {
cloudKitManager.fetchRecordsWith(type: Entry.TypeKey) { (records, error) in
if let error = error {
print(error.localizedDescription)
}
guard let records = records else { return } //Make sure there are records.
let entries = records.flatMap({Entry(cloudKitRecord: $0)})
self.entries = entries //This is connected to the private(set) property "entries"
}
}
private func saveToPersistentStorage() {
let entryRecords = self.entries.map({$0.cloudKitRecord})
cloudKitManager.save(records: entryRecords, perRecordCompletion: nil) { (records, error) in
if error != nil {
print(error?.localizedDescription as Any)
return
} else {
print("Successfully saved records to cloudKit")
}
}
}
// MARK: Properties
private(set) var entries = [Entry]() {
didSet {
DispatchQueue.main.async {
NotificationCenter.default.post(name: entriesWereSetNotification, object: nil)
}
}
}
}
Here's a couple threads that might be helpful.
If you were caching the data locally you would use the encodesystemfields method to create a new CKRecord that will update an existing one on the server.
How (and when) do I use iCloud's encodeSystemFields method on CKRecord?
It doesn't appear you are caching locally. I don't have experience doing it without using encodesystemfields, but it looks like you have to pull the record down and save it back in the completion handler of the convenience method:
Trying to modify ckrecord in swift