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
}
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.
(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.