0

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)
        }
    }
}
}
Josh
  • 643
  • 7
  • 11
  • Did you check that if you update a journal entry then the `record`'s `recordID` stays the same? If `recordID` is the same then CloudKit will not create a new record... – Ladislav Nov 03 '17 at 14:22
  • @Ladislav I don't see a recordID anywhere when I look on the cloudKit dashboard. – Josh Nov 03 '17 at 14:57
  • Each record has `Record Name` that is the name when you create a new `CKRecordID` for your CloudKit entry with `CKRecordID(recordName: "recordName")` or `CKRecordID(recordName: "recordName", zoneID: zone)` – Ladislav Nov 03 '17 at 15:02
  • Oh ok, I see what you're saying. So when I try to update a record, a new record with a new recordID is created. How would I make sure that cloudKit knows the only update the record with the specific recordID? Thanks - I'm pretty new to this! – Josh Nov 03 '17 at 15:05
  • In your `modify` method you have this `CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)`, records is an Array of `[CKRecord]`, so each record that you want to update you make sure its `record.recordID.recordName` is the same and not a new one... – Ladislav Nov 03 '17 at 15:07
  • I'm not sure If I am getting this right. -probably not because I haven't figured it out yet. I imagine I will do an if statement and check if two things are the same. I am not seeing the two things I need to compare because my `records` has no recordID component. Thanks – Josh Nov 03 '17 at 15:23
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/158174/discussion-between-josh-and-ladislav). – Josh Nov 03 '17 at 15:34

1 Answers1

0

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

Brian M
  • 3,812
  • 2
  • 11
  • 31