Why is CloudKit on Apple Watch super slow? - swift

I am using CloudKit to store very simple workout data. The quantity is negligible.
I am using the same code to interact with CloudKit for the iOS app as well as the watchOS app. This is the code I use for loading data:
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: recordType.rawValue, predicate: predicate)
let queryOperation = CKQueryOperation(query: query)
var results: [CKRecord] = []
queryOperation.recordFetchedBlock = { (record: CKRecord ) in
results.append(record)
}
queryOperation.queryCompletionBlock = { (cursor: CKQueryOperation.Cursor?, error: Error?) in
if let error = error {
print("Getting all " + recordType.rawValue + " records with CloudKit was unsuccessful", error)
response(false, nil)
return
}
if cursor == nil {
response(true, results)
return
}
let nextOperation = CKQueryOperation(cursor: cursor!)
nextOperation.recordFetchedBlock = queryOperation.recordFetchedBlock
nextOperation.queryCompletionBlock = queryOperation.queryCompletionBlock
privateDatabase.add(nextOperation)
}
privateDatabase.add(queryOperation)
On iOS the loading is almost instant, on watchOS this can take minutes which is basically unusable. Sporadically the loading speed on watchOS can be decent.
What could be the cause?

Concept
qualityOfService is set to default when you don't assign a configuration.
Assume the watch is low on battery then system decides whether to process the operation immediately or later.
So setting it explicitly might help the system determine how you would like the operation to be handled.
Code
Can you try setting the configuration as follows:
let configuration = CKOperation.Configuration()
configuration.qualityOfService = .userInitiated
queryOperation.configuration = configuration
queryOperation.queuePriority = .veryHigh //Use this wisely, marking everything as very high priority doesn't help

Related

perform(_:inZoneWith:completionHandler:) deprecated? or not? iOS 15

In Xcode 13 beta for iOS 15, I am receiving a message that perform(_:inZoneWith:completionHandler:) (CloudKit) is deprecated in iOS 15 and renamed to fetchRecords(matching:inZoneWith:desiredKeys:resultsLimit:completionHandler:) However...
The Apple Docs website does not declare this method as deprecated: https://developer.apple.com/documentation/cloudkit/ckdatabase/1449127-perform
Apple is showing other deprecations for iOS 15 (another method): https://developer.apple.com/documentation/cloudkit/ckdatabase/3794331-records/
fetchRecords(matching:inZoneWith:desiredKeys:resultsLimit:completionHandler:) does not seem to exist.. yet.. (Value of type 'CKDatabase' has no member 'fetchRecords')
So, is this just an incorrect message because its beta? Should I worry about rewriting a function that uses perform(_:inZoneWith:completionHandler:)?
Here is my function, I've tried to adapt it to fetchRecords, but it does not exist. I tried adapting it to fetch(withQuery:completionHandler: but I'm kind of lost getting it to work..
(This function just deletes records from the iCloud private database):
let container = CKContainer(identifier: "MyContainerNameHere")
let recordType = "DBName"
//delete all saved icloud records
let query = CKQuery(recordType: recordType, predicate: NSPredicate(value: true))
container.privateCloudDatabase.perform(query, inZoneWith: nil) { (rec, err) in
if let err = err {
print(err.localizedDescription)
completion(.failure(err))
return
}
guard let rec = rec else {
completion(.failure(CloudKitHelperError.castFailure))
return
}
for record in rec {
container.privateCloudDatabase.delete(withRecordID: record.recordID) { (recordId, err) in
if let err = err {
print(err.localizedDescription)
completion(.failure(err))
return
}
guard recordId != nil else {
completion(.failure(CloudKitHelperError.recordIDFailure))
return
}
}
}
}
Any insight appreciated..
Thx
Update
I will say, that yes, this seems to be an error or at least a premature message, however, after rewriting the code for async/await, it is much cleaner and easier to read. For those struggling to figure this out, here is an example of the code above converted to Async/Await:
#MainActor func newDeleteCloudKit() async throws {
let container = CKContainer(identifier: "MyContainerNameHere")
let recordType = "DBName"
let query = CKQuery(recordType: recordType, predicate: NSPredicate(value: true))
let result = try await container.privateCloudDatabase.records(matching: query)
for record in result.0 {
try await container.privateCloudDatabase.deleteRecord(withID: record.0)
}
}
I'm in beta 5 and I still get this warning, but the method hasn't been implemented, so it looks like they are not deprecating the old one and just forgot to remove the warning. We should have the final version of Xcode in a few days.
UPDATE: It looks like the made a mistake. The new method is not called fetchedRecords(), it is called records() https://developer.apple.com/documentation/cloudkit/ckdatabase/3856524-records

How to retrieve data from CloudKit using SwiftUI

I am using SwiftUI to make an app and storing the data on ICloud. Because all the code I can find relates to Swift and viewDidLoad and TableView, however this do not apply. I have written code and seems to retrieve it but will not return it to the ObservableObject to be able to display in the SwiftUI
The ObservableObject file:-
import SwiftUI
import Combine
final class ObservedData: ObservableObject {
#Published var venues = venuesData
}
The query to retrieve data
import SwiftUI
import CloudKit
var venuesData: [Venue] = loadVenues()
func loadVenues() -> [Venue] {
var data = [Venue]()
let pred = NSPredicate(value: true)
let sort = NSSortDescriptor(key: "id", ascending: true)
let query = CKQuery(recordType: "DeptfordHopData", predicate: pred)
query.sortDescriptors = [sort]
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["id","name", "address"]
operation.resultsLimit = 50
var newVenues = [Venue]()
operation.recordFetchedBlock = { record in
let venue = Venue()
venue.racordID = record.recordID
venue.id = record["id"]
venue.name = record["name"]
venue.address = record["address"]
newVenues.append(venue)
}
operation.queryCompletionBlock = {(cursor, error) in
DispatchQueue.main.async {
if error == nil {
data = newVenues
} else {
print(error!.localizedDescription)
}
}
}
CKContainer.default().publicCloudDatabase.add(operation)
return data
}
I have got data showing when do break in data but does not pass to venueData
The query operation runs asynchronously. When you add the operation to the database, the framework runs the query in the background and immediately returns control to your function, which promptly returns data, which is still set to the empty array.
To fix this, you either need to wait for the operation to complete using Grand Central Dispatch (not recommended if this is called directly from UI code, as it will block your application), or you need to rewrite loadVenues so it accepts a callback that you can then invoke inside queryCompletionBlock. The callback you provide to loadVenues should save the array of venues somewhere where SwiftUI will detect the change.

CKSubscription error iOS10

My subscriptions were working properly with iOS 9, but since I updated, I have a very odd error. I have two subscription methods that are equal, except for the fields they manage. Here is the code:
let meetingSubscriptionPredicate = Predicate(format: "Users CONTAINS %#", (id?.recordName)!)
let meetingSubscription = CKQuerySubscription(recordType: "Meetings", predicate: meetingSubscriptionPredicate, options: .firesOnRecordCreation)
let notification = CKNotificationInfo()
notification.alertBody = "Meeting Created!"
notification.shouldBadge = true
notification.accessibilityPerformEscape()
meetingSubscription.notificationInfo = notification
database.save(meetingSubscription) { (result, error) -> Void in
if error != nil {
print(error!.localizedDescription)
}
}
let universitiesSubscriptionPredicate = Predicate(format: "Name = %#", self.UniversityTextField.text!)
let universitiesSubscription = CKQuerySubscription(recordType: "Universities", predicate: universitiesSubscriptionPredicate, options: .firesOnRecordCreation)
let universitiesNotification = CKNotificationInfo()
universitiesNotification.alertBody = "Your university is now on Meet'em!"
universitiesNotification.shouldBadge = true
universitiesNotification.accessibilityPerformEscape()
universitiesSubscription.notificationInfo = universitiesNotification
database.save(universitiesSubscription, completionHandler: { (saved, error) in
if error != nil {
print(error!.localizedDescription)
}
else {
print("University subscription created")
}
})
The odd thing is that the Meeting subscription is saved, and the University's subscription is not. I've double checked the names and they are all right at the Dashboard. Besides that, I'm not getting any notification on my phone when supposed to...
Found elsewhere online that you can try to reset your development environment. In my case, the predicate I was trying to use was not set to queryable. This is ticked where you define your record type. The reason it worked one day and not another is possibly due to moving from development to production. At that time you are asked to optimize indexes, and it is possibly here that the ability to search on a given predicate is dropped. That seemed to be the case for me anyway.

HealthKit fetch data between interval

I have a little problem grasping HealthKit. I want to get heart rate from HealthKit with specific time.
I have done this in the past (until I noticed that I couldn't fetch data when the phone was locked)
func retrieveMostRecentHeartRateSample(completionHandler: (sample: HKQuantitySample) -> Void) {
let sampleType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
let predicate = HKQuery.predicateForSamplesWithStartDate(NSDate.distantPast() as! NSDate, endDate: NSDate(), options: HKQueryOptions.None)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor])
{ (query, results, error) in
if error != nil {
println("An error has occured with the following description: \(error.localizedDescription)")
} else {
let mostRecentSample = results[0] as! HKQuantitySample
completionHandler(sample: mostRecentSample)
}
}
healthKitStore.executeQuery(query)
}
var observeQuery: HKObserverQuery!
func startObservingForHeartRateSamples() {
println("startObservingForHeartRateSamples")
let sampleType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
if observeQuery != nil {
healthKitStore.stopQuery(observeQuery)
}
observeQuery = HKObserverQuery(sampleType: sampleType, predicate: nil) {
(query, completionHandler, error) in
if error != nil {
println("An error has occured with the following description: \(error.localizedDescription)")
} else {
self.retrieveMostRecentHeartRateSample {
(sample) in
dispatch_async(dispatch_get_main_queue()) {
let result = sample
let quantity = result.quantity
let count = quantity.doubleValueForUnit(HKUnit(fromString: "count/min"))
println("sample: \(count)")
heartChartDelegate?.updateChartWith(count)
}
}
}
}
healthKitStore.executeQuery(observeQuery)
}
This code will fetch the latest sample every time it is a change in HealthKit. But as I said earlier, it won't update when the phone is locked. I tried using:
self.healthKitStore.enableBackgroundDeliveryForType(HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate), frequency: HKUpdateFrequency.Immediate) { (success, error) in
if success{
println("success")
} else {
println("fail")
}
}
But this didn't work and as I found out there was a bug that Apple said it wasn't working as they wanted. Guess it is some security-thing.
But then I thought, maybe I can request samples between a startTime and endTime. For example i have EndTime(2015-05-31 10:34:45 +0000) and StartTime(2015-05-31 10:34:35 +0000).
So my question is how can I get heart rate samples between these two times.
I guess I must do it in the
HKQuery.predicateForSamplesWithStartDate(myStartTime, endDate: myEndTime, options: HKQueryOptions.None)
But when I tried it didn't find anything. Maybe I got this all wrong...
I am using a heart rate monitor on my chest and I know that I get some values in healthKit within the start and end time.
Edit:
Ok I tried it and it is working some times, not always. Someone has an idea?
func fetchHeartRates(endTime: NSDate, startTime: NSDate){
let sampleType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
let predicate = HKQuery.predicateForSamplesWithStartDate(startTime, endDate: endTime, options: HKQueryOptions.None)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: 100, sortDescriptors: [sortDescriptor])
{ (query, results, error) in
if error != nil {
println("An error has occured with the following description: \(error.localizedDescription)")
} else {
for r in results{
let result = r as! HKQuantitySample
let quantity = result.quantity
let count = quantity.doubleValueForUnit(HKUnit(fromString: "count/min"))
println("sample: \(count) : \(result)")
}
}
}
healthKitStore.executeQuery(query)
}
Edit 2:
It was working but I couldn't call it the way I did. So I fetched it a couple of seconds later and it worked fine :)
...But as I said earlier, it won't update when the phone is locked...Guess it is some security-thing.
You are correct.
From HealthKit Framework Reference:
Because the HealthKit store is encrypted, your app cannot read data from the store when the phone is locked. This means your app may not be able to access the store when it is launched in the background. However, apps can still write data to the store, even when the phone is locked. The store temporarily caches the data and saves it to the encrypted store as soon as the phone is unlocked.
If you want your app to be alerted when there are new results, you should review Managing Background Delivery:
enableBackgroundDeliveryForType:frequency:withCompletion:
Call this method to register your app for background updates. HealthKit wakes your app whenever new samples of the specified type are saved to the store. Your app is called at most once per time period defined by the specified frequency.

swift load all record from cloud kit avoiding limit result

guys need help i can't find something similar online even setting the limit result to a fixed value still doesn't work no matter what i always get 100 record max i need all the records in my database right now is 377 record and will be way more probably 10k but this process will only run once could someone help me catch all the record from the database avoiding the 100 limit please
i added the cursor as you suggested and now i have an idea but now it loads 200 results not the total any tip please
if foodList.count == 0{
loadingView.hidden = false
loadMainDatabaseActivity.startAnimating()
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "FoodListDataBase", predicate: predicate)
let operation = CKQueryOperation(query: query)
//operation.resultsLimit = CKQueryOperationMaximumResults
operation.recordFetchedBlock = { (record: CKRecord!) in
if record != nil{
count++
foodList.append(record.objectForKey("foodname") as! String)
categories.append(record.objectForKey("category") as! String)
}
}
operation.queryCompletionBlock = {(cursor: CKQueryCursor!, error: NSError!) in
if cursor != nil {
let newOperation = CKQueryOperation(cursor: cursor)
newOperation.recordFetchedBlock = operation.recordFetchedBlock
newOperation.queryCompletionBlock = operation.queryCompletionBlock
newOperation.resultsLimit = 300
publicDB!.addOperation(newOperation)
println(count)
}
self.loadingView.hidden = true
self.loadMainDatabaseActivity.stopAnimating()
}
publicDB.addOperation(operation)
}
I believe this was answered in the following post:
CKQuery from private zone returns only first 100 CKRecords from in CloudKit
Changing the CKQueryOperationMaximumResults to a larger number.