With the help of a few tutorials I coded my first iCloud App. It is working well in Xcode simulator and on my iPhone and iPad. But as soon as I upload it for TestFlight testing it isn't working anymore.
Here is the whole code for getting and uploading the data. It is a simple one-ViewController Shopping list App which has two arrays: listItems for the current shopping list and shopItems for all items which are added so far. These arrays are stored as string lists in the iCloud recordZone All data are stored locally on the device and in the cloud.
The App is checking the connectivity, the iCloud availability and the fact, if the shopping list was edited while being offline, before it gets the data from iCloud.
// Init all values
var listItems = [String]()
var shopItems = [String]()
var cloudCheck = true
var onlineCheck = true
// Init the user defaults
let defaults = UserDefaults.standard
let privateDatabase = CKContainer.default().privateCloudDatabase
let recordZone = CKRecordZone(zoneName: "ShopListZone")
let predicate = NSPredicate(value: true)
var editedRecord: CKRecord!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: true)
}
if (reachability?.isReachableViaWiFi)! || (reachability?.isReachableViaWWAN)! {
if isICloudContainerAvailable() && defaults.bool(forKey: "changed") == false {
getCloudData()
}
else if isICloudContainerAvailable() && defaults.bool(forKey: "changed") {
loadOffline()
}
else {
cloudCheck = false
}
} else {
onlineCheck = false
loadOffline()
}
}
// Get the record from iCloud
func getCloudData() {
// Connect to iCloud and fetch the data
let query = CKQuery(recordType: "ShopListData", predicate: predicate)
let operation = CKQueryOperation(query: query)
var myItems = [String]()
var allItems = [String]()
operation.recordFetchedBlock = { record in
myItems = record["ListItems"] as! [String]
allItems = record["ShopItems"] as! [String]
}
operation.queryCompletionBlock = { [unowned self] (cursor, error) in
DispatchQueue.main.async {
if error == nil {
self.listItems = myItems
self.shopItems = allItems
self.tableView.reloadData()
} else {
self.cloudCheck = false
print("iCloud load error: \(String(describing: error?.localizedDescription))")
}
}
}
privateDatabase.add(operation)
cloudCheck = true
}
// Upload and save the record to iCloud
#IBAction func uploadShopListData(_ sender: UIButton) {
// Save the shop list in the user defaults
defaults.set(listItems, forKey: "myItems")
// Set bool if saving while offline
if (reachability?.isReachableViaWiFi)! == false && (reachability?.isReachableViaWWAN)! == false {
defaults.set(true, forKey: "changed")
}
// Save the record
if cloudCheck && onlineCheck {
defaults.set(false, forKey: "changed")
saveRecord()
// Show a short message if records were saved successfully
self.myAlertView(title: "iCloud online", message: NSLocalizedString("Shop list was saved in iCloud.", comment: ""))
} else {
// Show a short message if iCloud isn't available
myAlertView(title: "iCloud offline", message: NSLocalizedString("Shop list was saved on iPhone.", comment: ""))
defaults.set(true, forKey: "changed")
}
}
// Save the shop lists
func saveRecord() {
// Connect to iCloud and start operation
let query = CKQuery(recordType: "ShopListData", predicate: predicate)
privateDatabase.perform(query, inZoneWith: recordZone.zoneID) {
allRecords, error in
if error != nil {
// The query returned an error
OperationQueue.main.addOperation {
print("iCloud save error: \(String(describing: error?.localizedDescription))")
// If there is now record yet, create a new one
self.createRecord()
}
} else {
// The query returned the records
if (allRecords?.count)! > 0 {
let newLists = allRecords?.first
newLists?["ListItems"] = self.listItems as CKRecordValue
newLists?["ShopItems"] = self.shopItems as CKRecordValue
self.privateDatabase.save(newLists!, completionHandler: { returnRecord, error in
if error != nil {
// Print an error message
OperationQueue.main.addOperation {
print("iCloud save error: \(String(describing: error?.localizedDescription))")
}
} else {
// Print a success message
OperationQueue.main.addOperation {
print("Shop list was saved successfully")
}
}
})
}
}
}
}
// Create a new record
func createRecord() {
let myRecord = CKRecord(recordType: "ShopListData", zoneID: (self.recordZone.zoneID))
let operation = CKModifyRecordsOperation(recordsToSave: [myRecord], recordIDsToDelete: nil)
myRecord.setObject(self.listItems as CKRecordValue?, forKey: "ListItems")
myRecord.setObject(self.shopItems as CKRecordValue?, forKey: "ShopItems")
operation.modifyRecordsCompletionBlock = { records, recordIDs, error in
if let error = error {
print("iCloud create error: \(String(describing: error.localizedDescription))")
} else {
DispatchQueue.main.async {
print("Records are saved successfully")
}
self.editedRecord = myRecord
}
}
self.privateDatabase.add(operation)
// Show a short message if icloud save was successfull
self.myAlertView(title: "iCloud online", message: NSLocalizedString("Shop list was saved in iCloud.", comment: ""))
}
Any idea, what did I wrong? I read in another post that I should change the iCloud dashboard from development to production, but others say that this should be done only when the App is already on the way to the App store ..
When you upload your app to TestFlight, it's done using live configuration and app will try to connect to production container.
You have two options to test on device:
1) deploy your iCloud schema to production. As you have nothing released yet, it will not break anything.
2) after you archive your project, export it for 'Development deployment'. You will be asked what iCloud Container Environment should be used. You can select 'Development' and install app locally using Apple Configurator
It was not clear to me that setting iCloud Dashboard to production means, that the record type is used, but the record zone I created during development not.
So I have to create the custom record zone first in the createRecord() method (see above). That's it.
Related
Overview
I have 2 cycling workouts (recorded using the Workouts app on an Apple Watch SE) that I'm trying to retrieve the location data (GPX samples) from. They were part of the same continuous ride that I recorded using 2 separate workouts due to the original workout being unable to unpause midway through. Ultimately, I'd like to obtain all location samples and merge them into a single file.
Current State
The health app on my iPhone (SE, 2020) correctly shows 2 workouts on the day I recorded them (July 16th), and also contains 4 workout route objects for the 2 workouts: 1 that makes up the entirety of the first workout, and 3 that combine to make up the second workout.
Screenshot showing 2 workouts
Screenshot showing 4 workout routes
When I export the raw health data however, only 3 workout routes show up for July 16th. And they're all for the second workout. No GPX route file is exported for the first workout.
Attempt to Solve
In an attempt to try and access the raw data from HealthKit directly, I found an app on Github, built it, and loaded it onto my iPhone to see what I could get. The app retrieves a list of workouts, and exports a GPX file with all location samples when a workout is selected. This works great with the second of my cycling workouts, but crashes when trying to export the data from the first workout.
Here's the function that actually queries the workout for route information:
public func route(for workout: HKWorkout, completion: #escaping (([CLLocation]?, Error?) -> Swift.Void)) {
let routeType = HKSeriesType.workoutRoute();
let p = HKQuery.predicateForObjects(from: workout)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let q = HKSampleQuery(sampleType: routeType, predicate: p, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) {
(query, samples, error) in
if let err = error {
print(err)
return
}
guard let routeSamples: [HKWorkoutRoute] = samples as? [HKWorkoutRoute] else { print("No route samples"); return }
if (routeSamples.count == 0){
completion([CLLocation](), nil)
return;
}
var sampleCounter = 0
var routeLocations:[CLLocation] = []
for routeSample: HKWorkoutRoute in routeSamples {
let locationQuery: HKWorkoutRouteQuery = HKWorkoutRouteQuery(route: routeSample) { _, locationResults, done, error in
guard locationResults != nil else {
print("Error occured while querying for locations: \(error?.localizedDescription ?? "")")
DispatchQueue.main.async {
completion(nil, error)
}
return
}
if done {
sampleCounter += 1
if sampleCounter != routeSamples.count {
if let locations = locationResults {
routeLocations.append(contentsOf: locations)
}
} else {
if let locations = locationResults {
routeLocations.append(contentsOf: locations)
let sortedLocations = routeLocations.sorted(by: {$0.timestamp < $1.timestamp})
DispatchQueue.main.async {
completion(sortedLocations, error)
}
}
}
} else {
if let locations = locationResults {
routeLocations.append(contentsOf: locations)
}
}
}
self.healthStore.execute(locationQuery)
}
}
healthStore.execute(q)
}
After further debugging, it appears that the HKSampleQuery call is returning 0 samples when querying the first cycling workout. It returns 3 samples (each of the workout route objects) when querying the second cycling workout.
So it's as if the HealthKit API can't see the 4th workout route, even though it clearly exists as evidenced by the screenshot from the health app.
Question
What's going on here? Why does the second workout return data just fine but the first doesn't?
Is it possible to query the health database directly to obtain this data? I have a local encrypted iPhone backup with a known passcode I can access as well.
This cycling ride was pretty important to me, so I'm trying all I can to retrieve the location data. Thanks in advance!
Edit: Adding main code where the route() function gets called.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath);
guard let workouts = self.workouts else {
return;
}
if (indexPath.row >= workouts.count){
return;
}
print(indexPath.row)
let workout = workouts[indexPath.row];
let workout_name: String = {
switch workout.workoutActivityType {
case .cycling: return "Cycle"
case .running: return "Run"
case .walking: return "Walk"
default: return "Workout"
}
}()
let workout_title = "\(workout_name) - \(self.dateFormatter.string(from: workout.startDate))"
let file_name = "\(self.filenameDateFormatter.string(from: workout.startDate)) - \(workout_name)"
let targetURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(file_name)
.appendingPathExtension("gpx")
let file: FileHandle
do {
let manager = FileManager.default;
if manager.fileExists(atPath: targetURL.path){
try manager.removeItem(atPath: targetURL.path)
}
print(manager.createFile(atPath: targetURL.path, contents: Data()))
file = try FileHandle(forWritingTo: targetURL);
} catch let err {
print(err)
return
}
workoutStore.heartRate(for: workouts[indexPath.row]){
(rates, error) in
guard let keyedRates = rates, error == nil else {
print(error as Any);
return
}
let iso_formatter = ISO8601DateFormatter()
var current_heart_rate_index = 0;
var current_hr: Double = -1;
let bpm_unit = HKUnit(from: "count/min")
var hr_string = "";
file.write(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><gpx version=\"1.1\" creator=\"Apple Workouts (via pilif's hack of the week)\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.topografix.com/GPX/1/1\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\" xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v1\"><trk><name><![CDATA[\(workout_title)]]></name><time>\(iso_formatter.string(from: workout.startDate))</time><trkseg>"
.data(using: .utf8)!
)
self.workoutStore.route(for: workouts[indexPath.row]){
(maybe_locations, error) in
guard let locations = maybe_locations, error == nil else {
print(error as Any);
file.closeFile()
return
}
for location in locations {
while (current_heart_rate_index < keyedRates.count) && (location.timestamp > keyedRates[current_heart_rate_index].startDate) {
current_hr = keyedRates[current_heart_rate_index].quantity.doubleValue(for: bpm_unit)
current_heart_rate_index += 1;
hr_string = "<extensions><gpxtpx:TrackPointExtension><gpxtpx:hr>\(current_hr)</gpxtpx:hr></gpxtpx:TrackPointExtension></extensions>"
}
file.write(
"<trkpt lat=\"\(location.coordinate.latitude)\" lon=\"\(location.coordinate.longitude)\"><ele>\(location.altitude.magnitude)</ele><time>\(iso_formatter.string(from: location.timestamp))</time>\(hr_string)</trkpt>"
.data(using: .utf8)!
)
}
file.write("</trkseg></trk></gpx>".data(using: .utf8)!)
file.closeFile()
let activityViewController = UIActivityViewController( activityItems: [targetURL],
applicationActivities: nil)
if let popoverPresentationController = activityViewController.popoverPresentationController {
popoverPresentationController.barButtonItem = nil
}
self.present(activityViewController, animated: true, completion: nil)
}
}
}
I have a collection on Firestore and I listen for changes like this:
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
})
}
I only want to listen for documents that are actually added to that collection.
In fact, the problem is that whenever I invoke this function I receive all the documents of the collection as added documents and then I also receive documents added later.
How can I listen just for actually added later documents, ignoring the ones already present in the collection? Searching online I didn't find any solution to this issue.
EDIT:
This is the way I tried to solve the problem:
func createMatchesListener(){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
getUidsAlreadyMade { uidsAlreadyMade in
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
let data = change.document.data()
let userId = data["uid"] as? String ?? ""
if uidsAlreadyMade.contains(userId) == false{
//means the uid is newly created in the collection, do stuff accordingly
arrayOfUidsAlreadyMade.append(currentUid)
}
}
if change.type == .removed{
// if the document has been removed, remove also the id from the array of uids
let data = change.document.data()
let currentUid = data["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.removeAll { $0 == currentUid }
}
})
})
}
}
func getUidsAlreadyMade(completion: #escaping ([String]) -> Void){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
db.collection("Matches").document(currentUid).collection("Matches").getDocuments { snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
arrayOfUidsAlreadyMade.removeAll()
snapshot?.documents.forEach({ doc in
let dict = doc.data()
let userId = dict["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.append(userId)
})
completion(arrayOfUidsAlreadyMade)
}
}
A simple solution is to include a timestamp in your Firestore documents.
Suppose your documents store Tasks, for example
documentId
task: "get dinner"
timestamp: 20211123
and suppose your app doesn't care about past tasks, only new ones.
When the tasks are read, update the timestamp as to when that occurred.
Then each time after that you want to read only 'new data' specify that in your listener, keeping track of when the last read timestamp was:
db.collection("task").whereField("timestamp", isGreaterThan: lastReadTimeStamp).addSnapshotListener...
The above will only read in tasks that occured after the prior timestamp and add a Listener (reading in all of the new tasks so you can populate the UI).
You can store an array with the ID of the documents that you already have stored in the device. That way, all that you need to do before doing things is checking that document's id is not in your array
There's no way of preventing Firestore from returning the initial snapshot of documents when a document listener is added, so just use a boolean to keep track of the initial snapshot and ignore it.
var listenerDidInit = false
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
if listenerDidInit {
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
} else {
listenerDidInit = true
}
})
}
private var listener: ListenerRegistration?
self.listener = db.collection("Matches") // matchesListener
listener!.remove()
I am able to delete the logged in users document when my app is first loaded. However, if I create a new entry, long press the new entry from the table view and select delete, I get a crash. I thought it had something to do with the document ID not being saved but I couldn't figure out why. If the same newly created entry is deleted after the app is closed and reopened then it will delete with no problem, but if I leave the app open and delete immediately after creating a new document, it will crash.
class BudgetViewController: UIViewController: {
var budgetData = [Transaction]()
func showAdd() {
let modalViewController = AddCategory()
modalViewController.addCategoryCompletion = { newCategories in
self.budgetData.append(newCategories)
self.tableView.reloadData()
}
modalViewController.modalPresentationStyle = .overFullScreen
modalViewController.modalTransitionStyle = .crossDissolve
modalViewController.selectionDelegate = self
present(modalViewController, animated: true, completion: nil)
}
#objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer){
if gestureRecognizer.state == .began {
let touchPoint = gestureRecognizer.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
let cell = CategoryCell()
var data = budgetData[indexPath.row]
let modalViewController = EditCategory()
modalViewController.deleteCategory = { row in
self.deletedRow = row
self.deleteRow()
}
modalViewController.documentID = data.trailingSubText ?? ""
modalViewController.modalPresentationStyle = .overFullScreen
modalViewController.modalTransitionStyle = .crossDissolve
present(modalViewController, animated: true, completion: nil)
modalViewController.row = indexPath.row
print("longpressed\(indexPath.row)\(data.trailingSubText)")
}
}
}
override func viewDidLoad() {
loadNewData()
}
func loadNewData() {
guard let user = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(user).collection("Category").getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
for document in snapshot!.documents {
let data = document.data()
let title = data["title"] as? String ?? ""
let uid = data["uid"] as? String ?? ""
let documentID = document.documentID
// let timeStamp = data["timeStamp"] as? Date
let newSourse = Transaction(title: title, dateInfo: "0% out of spent", image: UIImage.gymIcon, amount: 12, annualPercentageRate: 12, trailingSubText: documentID, uid: uid)
self.budgetData.append(newSourse)
}
self.tableView.reloadData()
}
}
}
class AddCategory: UIViewController {
#objc func saveAction(){
guard let uid = Auth.auth().currentUser?.uid else { return }
let newCategory = Transaction(title: textField.text ?? "", dateInfo: "0% out of spent", image: UIImage.gymIcon, amount: 12, annualPercentageRate: 23, trailingSubText: "", uid: uid)
db.collection("users").document(uid).collection("Category").addDocument(data: newCategory.dictionary)
self.dismiss(animated: false, completion: {
self.addCategoryCompletion?(newCategory)
})
self.dismiss(animated: false, completion: nil)
print("selected")
}
}
}
class EditCategory: UIViewController {
func deleteAction(){
guard let user = Auth.auth().currentUser?.uid else { return }
print("document::\(self.documentID)")
// let budget = textField.text
db.collection("users").document(user).collection("Category").document(documentID).delete { (err) in
if let err = err {
print(err.localizedDescription)
}else{
self.dismiss(animated: false, completion: {
self.deleteCategory?(self.row)
})
print("deleted successfully")
}
}
}
}
The error is strongly suggesting that user is nil or empty at the time you run this code:
guard let user = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(user).collection("Category").getDocuments()
This almost certainly means that a user was not signed in at the time. Your code needs to check currentUser for nil before trying to access its uid property. nil means that no user is currently signed in.
The user will not be signed in immediately at app launch. You should use an auth state listener to get a callback when the user object becomes available.
Maybe not the best approach, but it is working now. I called
self.budgetData.removeAll()
self.loadNewData()
self.tableView.reloadData()
inside of my callback
modalViewController.addCategoryCompletion = { newCategories in
self.budgetData.append(newCategories)
self.tableView.reloadData()
}`
Based on the presented code, when a new category is added to Firebase
let newCategory = Transaction(title: textField.text ?? ...)
db.collection("users").document(uid).collection("Category").addDocument(data: newCategory
you're not getting a valid documentId from Firebase first. So therefore that object exists in your dataSource with no documentId so when you try to remove it, there's a crash.
A couple of options
Option 1: Create a firebase reference first, which will provide a Firebase documentId that you can add to the object when writing. See the docs. Like this
let newCategoryRef = db.collection("Category").document()
let docId = newCategoryRef.documentId
...add docId to the category object, then add to dataSource
or
Option 2: Add an observer to the node (see Realtime Updates) so when a new document is written, the observers event will fire and present the newly added document, which will contain a valid documentId, and then craft an category object based on that data and add that object to your dataSource array. In this case, you don't need to add it to the dataSource array when writing as it will auto-add after it's written based on the observers .added event.
As I understand it, the CKModifyRecordsOperation(recordsToSave:, recordsToDelete:) method should make it possible to modify multiple records and delete multiple records all at the same time.
In my code, recordsToSave is an array with 2 CKRecords. I have no records to delete, so I set recordsToDelete to nil. Perplexingly enough, it appears that recordsToSave[0] gets saved to the cloud properly while recordsToSave[1] does not.
To give some more context before I paste my code:
In my app, there's a "Join" button associated with every post on a feed. When the user taps the "Join" button, 2 cloud transactions occur: 1) the post's reference gets added to joinedList of type [CKReference], and 2) the post's record should increment its NUM_PEOPLE property. Based on the CloudKit dashboard, cloud transaction #1 is occurring, but not #2.
Here is my code, with irrelevant parts omitted:
#IBAction func joinOrLeaveIsClicked(_ sender: Any) {
self.container.fetchUserRecordID() { userRecordID, outerError in
if outerError == nil {
self.db.fetch(withRecordID: userRecordID!) { userRecord, innerError in
if innerError == nil {
var joinedList: [CKReference]
if userRecord!.object(forKey: JOINED_LIST) == nil {
joinedList = [CKReference]() // init an empty list
}
else {
joinedList = userRecord!.object(forKey: JOINED_LIST) as! [CKReference]
}
let ref = CKReference(recordID: self.post.recordID, action: .none)
// ... omitted some of the if-else if-else ladder
// add to list if you haven't joined already
else if !joinedList.contains(ref) {
// modifying user record
joinedList.append(ref) // add to list
userRecord?[JOINED_LIST] = joinedList as CKRecordValue // associate list with user record
// modifying post
let oldCount = self.post.object(forKey: NUM_PEOPLE) as! Int
self.post[NUM_PEOPLE] = (oldCount + 1) as CKRecordValue
let operation = CKModifyRecordsOperation(recordsToSave: [userRecord!, self.post], recordIDsToDelete: nil)
self.db.add(operation)
}
// omitted more of the if-else if-else ladder
else {
if let error = innerError as? CKError {
print(error)
}
}
}
}
else {
if let error = outerError as? CKError {
print(error)
}
}
}
}
EDIT
Here's the code I added per the request of the first commenter
operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if error == nil {
DispatchQueue.main.async(execute: {
self.num.text = String(oldCount + 1) // UI update
})
}
else {
print(error!)
}
}
ANOTHER EDIT
let operation = CKModifyRecordsOperation(recordsToSave: [userRecord!, self.post], recordIDsToDelete: nil)
operation.perRecordCompletionBlock = { record, error in
if error != nil {
let castedError = error as! NSError
print(castedError)
}
}
operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if error == nil {
DispatchQueue.main.async(execute: {
self.num.text = String(oldCount + 1) // UI update
})
}
else {
print(error!)
}
}
self.db.add(operation)
So I want to wipe every record for a particular record type every day. So basically, I want the data to be wiped at 12:00 AM so that it will be fresh for the next day. How would I go about doing this? Is this something that I could set up in the CloudKit dashboard or will I have to set this up programmatically?
Deleting records from the dashboard is a lot of work if you need to delete multiple records.
The best workaround is by creating a separate recordType that will contain one record for every day. Then in the records that you want deleted for that day set up a CKReference to that particular day record and set its action to CKReferenceAction.DeleteSelf
After that you only have to remove the day record and all related records will be removed. Removing that one record could easily be done from the dashboard or you could create functionality in your app or you could create a 2nd app for administrative actions.
func deleteAllRecords()
{
let publicDatabase: CKDatabase = CKContainer.defaultContainer().publicCloudDatabase
// fetch records from iCloud, get their recordID and then delete them
var recordIDsArray: [CKRecordID] = []
let operation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDsArray)
operation.modifyRecordsCompletionBlock = {
(savedRecords: [CKRecord]?, deletedRecordIDs: [CKRecordID]?, error: NSError?) in
print("deleted all records")
}
publicDatabase.add(operation)
}
Try something like this:
let publicDb = CKContainer.defaultContainer().publicCloudDatabase
let query = CKQuery(recordType: "RECORD TYPE", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicDb.performQuery(query, inZoneWithID: nil) { (records, error) in
if error == nil {
for record in records! {
publicDb.deleteRecordWithID(record.recordID, completionHandler: { (recordId, error) in
if error == nil {
//Record deleted
}
})
}
}
}
"RECORD TYPE" should be your record type. Hope this helps.
this code able to delete any amount of records
import CloudKit
class iCloudDelete {
private let cloudDB: CKDatabase
private var recordIDsToDelete = [CKRecordID]()
private var onAllQueriesCompleted : (()->())?
public var resultsLimit = 10 // default is 100
init(cloudDB: CKDatabase){
self.cloudDB = cloudDB
}
func delete(query: CKQuery, onComplete: #escaping ()->Void) {
onAllQueriesCompleted = onComplete
add(queryOperation: CKQueryOperation(query: query))
}
private func add(queryOperation: CKQueryOperation) {
queryOperation.resultsLimit = resultsLimit
queryOperation.queryCompletionBlock = queryDeleteCompletionBlock
queryOperation.recordFetchedBlock = recordFetched
cloudDB.add(queryOperation)
}
private func queryDeleteCompletionBlock(cursor: CKQueryCursor?, error: Error?) {
print("-----------------------")
delete(ids: recordIDsToDelete) {
self.recordIDsToDelete.removeAll()
if let cursor = cursor {
self.add(queryOperation: CKQueryOperation(cursor: cursor))
} else {
self.onAllQueriesCompleted?()
}
}
}
private func recordFetched(record: CKRecord) {
print("RECORD fetched: \(record.recordID.recordName)")
recordIDsToDelete.append(record.recordID)
}
private func delete(ids: [CKRecordID], onComplete: #escaping ()->Void) {
let delete = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: ids)
delete.completionBlock = {
onComplete()
}
cloudDB.add(delete)
}
}