Using CKAsset to store API JSON and Updating every 6 Hours

309 Views Asked by At

I'm building a native iOS app with Swift/Xcode that utilizes JOSN data from a 3rd party API. Everything is working fine but the API has restrictions on the number of calls you can make to the API each hour. So now I'm building a function intended to create a weekly database as a CK Asset in CloudKit which will update the JSON from the API every 6 hours. This way the data is still relatively current but also reduces the number of API calls to just 4 a day.

Note: The app is being tested in Production/TestFlight.

The function is working correctly when creating a new CKAsset to save as a new CKRecord in CloudKit. The function also correctly downloads and decodes the CKAsset from CloudKit for use in the app. <- And anyone is able to download this asset and it works just fine.

The issue: Whenever the function checks if the CKAsset is more than 6 hours old, it is supposed to let any user modify the CKRecord by downloading a newer JSON file and replacing it in the CKAsset of the CKRecord using the CKModifyRecordsOperation. The problem is, whenever another user tries to modify the record the app crashes.

Question: Why can't any other users using TestFlight modify the record? Am I wrong to use CKModifyRecordsOperation?

Any help or suggestions is greatly appreciated!

-------------------------------CODE/FUNC--------------------------------

func fetchWeeklyPlayersDB() {
let semaphore = DispatchSemaphore.init(value: 0)
let thisWeek = getCurrentWeekID()
let current = Date()

// fetch current week PlayersDB from CK Database
publicDB.fetch(withRecordID: CKRecord.ID(recordName:thisWeek + "_Players"))  { record, error in
    // process record
    if let r = record { // we know the playersDB has data
        let modified = r.modificationDate
        let expirationDate = modified?.addingTimeInterval(6*60*60) // add 6 hours
        // if CK DB expirationDate is less than now, use it
        if expirationDate! > current {
            // not outdated - just process
            if let data = r["DB"] {
                // decode the JSON and set it to rawData
                let d = readCKAsset(asset: data as! CKAsset)
                let result = try? JSONDecoder().decode(dfsAPIData.self, from: d as! Data)
                rawData = result
                semaphore.signal()
            }
            
        } else { // CK DB is more than 6 hours old, call api and overwite data
            // call API
            let apiData = callAPI(week: findWeek())

            // encode the api data as NSData
            let jsonEncoder = JSONEncoder()
            do {
                let jsonData = try jsonEncoder.encode(apiData)
                // save data locally
                if let path = saveJSON(data: jsonData) {
                    // convert result to CKASset using local save filepath
                    let asset:CKAsset = CKAsset.init(fileURL: path)
                    r["DB"] = asset
                    // Modify PlayersDB value in CKRecord
                    let modifyRecord = CKModifyRecordsOperation(recordsToSave: [r], recordIDsToDelete: nil)
                    modifyRecord.savePolicy = CKModifyRecordsOperation.RecordSavePolicy.allKeys
                    modifyRecord.qualityOfService = QualityOfService.userInitiated
                    modifyRecord.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
                        if error == nil {
                            // we did it!
                            print("PlayersDB Successfully overwritted with update api data")
                            // delete the file you created
                            deleteJSON(path: path)
                            rawData = apiData
                            semaphore.signal()
                        } else {
                            print("ERROR SAVING PlayersDB TO CK" + error!.localizedDescription)
                            // delete the file you created
                            deleteJSON(path: path)
                            // pull from the CK DB anyway so it fails softly
                            if let data = r["DB"] {
                                // decode the JSON and set it to rawData
                                let d = readCKAsset(asset: data as! CKAsset)
                                let result = try? JSONDecoder().decode(dfsAPIData.self, from: d as! Data)
                                rawData = result
                                semaphore.signal()
                            }
                        }
                    }
                    publicDB.add(modifyRecord)
                }
            }
            catch {
                print("Error Encoding JSON - WTF")
                // if encoding fails - pull latest db instead to fail softly
                if let data = r["DB"] {
                    // decode the JSON and set it to rawData
                    let d = readCKAsset(asset: data as! CKAsset)
                    let result = try? JSONDecoder().decode(dfsAPIData.self, from: d as! Data)
                    rawData = result
                    semaphore.signal()
                }
            }
        }
    }
    
    // process error - DB doesnt exist, Call API and Create It
    if let e = error {
        // call API
        let apiData = callAPI(week: findWeek())
        // create record
        let recordID = CKRecord.ID(recordName:thisWeek + "_Players")
        let record = CKRecord(recordType: "WeeklyDB", recordID: recordID)
        // encode the api data as NSData
        let jsonEncoder = JSONEncoder()
        do {
            let jsonData = try jsonEncoder.encode(apiData)
            if let path = saveJSON(data: jsonData) {
                // convert result to CKASset using local save filepath
                let asset:CKAsset = CKAsset.init(fileURL: path)
                record["DB"] = asset
                    // Save DB to CK
                    publicDB.save(record, completionHandler: {returnRecord, error in
                        if let err = error {
                            // something happened
                            print("ERROR SAVING PlayersDB TO CK" + err.localizedDescription)
                        } else {
                            // we did it!
                            print("PlayersDB Successfully overwritted with update api data")
                            // delete the file you just created
                            deleteJSON(path: path)
                            rawData = apiData
                            semaphore.signal()
                        }
                    })
            }
        }
        catch {
            print("Error Encoding JSON while saving api data to PlayersDB - WTF")
        }
    }
}
semaphore.wait()
return

}

1

There are 1 best solutions below

0
On

So the solution to this answer comes in three parts because there were essentially three issues I was encountering.

One - When I first created my function I was using the CK datatype "Bytes" and "String" to save my JSON, but I was getting an error because the size of my JSON was over the 1mb limit those datatypes conform to. I didn't realize the size limit was causing the error, instead I thought it was the Save feature that was not working, so I switched to using the CKModifyRecord to try saving. Once that failed I noticed the size limit and switched to using CKAsset to save the database. This fixed the limit, but I still received errors when either saving or modifying the asset of the CK record.

Two - Since size was no longer the issue, I did some digging and it turns out security permissions in CloudKit was denying other users from saving a record they didn't create. To fix this I went into CK Dashboard and found the "Security Roles" under the "Schema" section. I switched the roles from "Creator" to "iCloud" to allow all connected iCloud users to edit records.

Three - The final problem I had was with identifying the errors I was getting from my TestFlight Testers. Until this point I had never tried to access the Crash Logs from TestFlight users and I though it was a long and convoluted process. Nope - its built into Xcode out of the box, not configuration required. (1) With Xcode open, click on Window > Organizer in the top menu to open the Organizer Window.

enter image description here

(2) From here you can see all crash logs from each build you've archived and submitted to test flight. And, you can even run them in Xcode to find the line/error that crashed the program by clicking "Open in Xcode" button.

enter image description here