iOS: CloudKit perform(query: ) does nothing - closure not executed - ios

I am in process in adding CloudKit to my app to enable iCloud sync. But I ran into problem with my method, that executes query with perform method on private database.
My method worked fine, I then changed a few related methods (just with check if iCloud is available) and suddenly my perform method does nothing. By nothing I mean that nothing in perform(query: ) closure gets executed. I have breakpoint on the first line and others on the next lines but never manage to hit them.
private static func getAppDetailsFromCloud(completion: #escaping (_ appDetails: [CloudAppDetails]?) -> Void) {
var cloudAppDetails = [CloudAppDetails]()
let privateDatabase = CKContainer.default().privateCloudDatabase
let query = CKQuery(recordType: APPID_Type, predicate: NSPredicate(format: "TRUEPREDICATE"))
privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
if let error = error {
print(error)
completion(nil)
} else {
if let records = records {
for record in records {
let appId = record.object(forKey: APPID_ID_Property) as? Int
let isDeleted = record.object(forKey: APPID_ISDELETED_Property) as? Int
if let appId = appId, let isDeleted = isDeleted {
cloudAppDetails.append(CloudAppDetails(id: appId, isDeleted: isDeleted == 1))
}
}
completion(cloudAppDetails)
return
}
}
completion(nil)
}
}
My problem starts at privateDatabase.perform line, after that no breakpoints are hit and my execution moves to function which called this one getAppDetailsFromCloud. There is no error...
This is my first time implementing CloudKit and I have no idea why nothing happens in the closure above.
Thanks for help.
EDIT: Forgot to mention that this metod used to work fine and I was able to get records from iCloud. I have not made any edits to it and now it does not work as described :/
EDIT 2: When I run the app without debugger attached then everything works flawlessly. I can sync all data between devices as expected. When I try to debug the code, then I once again get no records from iCloud.

In the completion handler shown here, if there's no error and no results are found, execution will fall through and quietly exit. So, there are two possible conditions happening here: the query isn't running or the query isn't finding any results. I'd perform the following investigative steps, in order:
Check your .entitlements file for the key com.apple.dev.icloud-container-environment. If this key isn't present, then builds from xcode will utilize the development environment. If this key is set, then builds from xcode will access the environment pointed to by this key. (Users that installed this app from Testflight or the app store will always use the production environment).
Open the cloudkit dashboard in the web browser and validate that the records you expect are indeed present in the environment indicated by step 1 and the container you expect. If the records aren't there, then you've found your problem.
If the records appear as expected in the dashboard, then place the breakpoint on the .perform line. If the query is not being called when you expected, then you need to look earlier in the call stack... who was expected to call this function?
If the .perform is being called as expected, then add an else to the if let record statement. Put a breakpoint in the else block. If that fires, then the query ran but found no records.
If, after the above steps, you find that the completion handler absolutely isn't executed, this suggests a malformed query. Try running the query by hand using the cloudkit dashboard and observing the results.

The closure executes asynchronously and usually you need to wait few seconds.
Take into account you can't debug many threads in same way as single. Bcs debugger will not hit breakpoint in closure while you staying in your main thread.

2019, I encountered this issue while working on my CloudKit tasks. Thunk's selected answer didn't help me, so I guess I'm gonna share here my magic. I got the idea of removing the breakpoints and print the results instead. And it worked. But I still need to use breakpoints inside the closure. Well, what I had to do is restart the Xcode. You know the drill in iOS development, if something's not right, restart the Xcode, reconnect the device, and whatnot.

Related

ambiguous for type lookup in this context when -com.apple.CoreData.ConcurrencyDebug 1 is active

The code below runs perfectly fine when I have the above parameter turned off in my Scheme. When it is turned on I get a 'Group' is ambiguous for type lookup in this context crash error. on the line "let currentGroup = context.object(with: groupID) as? Group"
I've checked my project and there is no duplicate reference to Group NSManagedObject.
let context = CoreDataStack.shared.newPrivateContext()
if reset {
AppDefault.current_ListGroup = nil
}
if let groupID = AppDefault.current_ListGroup,
let currentGroup = context.object(with: groupID) as? Group {
return currentGroup.objectID
} else {
Can someone help me figure out why it works with the .ConcurrencyDebug 1 off but crashes when it is on?
Thanks in Advance
When concurrency debugging is on, the app will crash any time you break the concurrency rules. Breaking the rules on its own doesn't always crash the app-- but with debugging enabled, you're saying that you want to crash as soon as you break the rules, even if the app would work normally without debugging. This is a good thing, because breaking the rules will probably make the app crash eventually even if it doesn't happen right now.
How you're breaking the rules here is:
You're creating a new private queue context with newPrivateContext.
You're using that context without calling perform or performAndWait.
With a private queue context, you must use one of those functions whenever you use the context. Really the only time you don't have to use one of those is if you're using main queue concurrency and you know that your code is running on the main queue. You can sometimes get away with not doing that, if everything is just right, but concurrency debugging will stop you immediately. That's what you're seeing.

How to handle first time launch experience when iCloud is required?

I am using CloudKit to store publicly available data and the new NSPersistentCloudKitContainer as part of my Core Data stack to store/sync private data.
When a user opens my app, they are in 1 of 4 states:
They are a new user with access to iCloud
They are a returning user with access to iCloud
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
States 1 and 2 represent my happy paths. If they are a new user, I'd like to seed the user's private store with some data before showing the initial view. If they are a returning user, I'd like to fetch data from Core Data to pass to the initial view.
Determining new/old user:
My plan is to use NSUbiquitousKeyValueStore. My concern with this is handling the case where they:
download the app -> are recorded as having launched the app before -> delete and reinstall/install the app on a new device
I assume NSUbiquitousKeyValueStore will take some time to receive updates so I need to wait until it has finished synchronizing before moving onto the initial view. Then there's the question of what happens if they don't have access to iCloud? How can NSUbiquitousKeyValueStore tell me if they are a returning user if it can't receive the updates?
Determining iCloud access:
Based on the research I've done, I can check if FileManager.default.ubiquityIdentityToken is nil to see if iCloud is available, but this will not tell me why. I would have to use CKContainer.default().accountStatus to learn why iCloud is not available. The issue is that is an asynchronous call and my app would have moved on before learning what their account status is.
I'm really scratching my head on this one. What is the best way to gracefully make sure all of these states are handled?
There's no "correct" answer here, but I don't see NSUbiquitiousKeyValueStore being a win in any way - like you said if they're not logged into iCloud or don't have network access it's not going to work for them anyway. I've got some sharing related stuff done using NSUbiquitiousKeyValueStore currently and wouldn't do it that way next time. I'm really hoping NSPersistentCloudKitContainer supports sharing in iOS 14 and I can just wipe out most of my CloudKit code in one fell swoop.
If your app isn't functional without cloud access then you can probably just put up a screen saying that, although in general that's not a very satisfying user experience. The way I do it is to think of the iCloud sync as truly asynchronous (which it is). So I allow the user to start using the app. Then you can make your call to accountStatus to see if it's available in the background. If it is, start a sync, if it's not, then wait until it is and then start the process.
So the user can use the app indefinitely standalone on the device, and at such time as they connect to the internet everything they've done on any other device gets merged into what they've done on this new device.
I struggled with this problem as well just recently. The solution I came up with was to query iCloud directly with CloudKit and see if it has been initialized. It's actually very simple:
public func checkRemoteData(completion: #escaping (Bool) -> ()) {
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Root'")
let query = CKQuery(recordType: .init("CD_Container"), predicate: predicate)
db.perform(query, inZoneWith: nil) { result, error in
if error == nil {
if let records = result, !records.isEmpty {
completion(true)
} else {
completion(false)
}
} else {
print(error as Any)
completion(false)
}
}
}
This code illustrates a more complex case, where you have instances of a Container entity with a derived model, in this case called Root. I had something similar, and could use the existence of a root as proof that the data had been set up.
See here for first hand documentation on how Core Data information is brought over to iCloud: https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/reading_cloudkit_records_for_core_data
to improve whistler's solution on point 3 and 4,
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
one should use UserDefaults as well, so that it covers offline users and to have better performance by skipping network connections when not needed, which is every time after the first time.
solution
func isFirstTimeUser() async -> Bool {
if UserDefaults.shared.bool(forKey: "hasSeenTutorial") { return false }
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Item'")
let query = CKQuery(recordType: "CD_Container", predicate: predicate)
do {
let items = (try await db.records(matching: query)).matchResults
return items.isEmpty
} catch {
return false
// this is for the answer's simplicity,
// but obviously you should handle errors accordingly.
}
}
func showTutorial() {
print("showing tutorial")
UserDefaults.shared.set(true, forKey: "hasSeenTutorial")
}
As it shows, after the first time user task showTutorial(), UserDefaults's bool value for key "hasSeenTutorial" is set to true, so no more calling expensive CK... after.
usage
if await isFirstTimeUser() {
showTutorial()
}

CloudKit method call hung up

When app starts some preliminary process take place. Sometimes it is done quickly in some second, and sometimes It does not end, but without any error it hung up.
I.e. at launch client always fetch the last serverChangedToken. and sometimes it just hung up it does not complete. I am talking about production environment, developer works well. So this route get called, but some times it does not finishes. Any idea why? I do not get any error, timeout.
let fnco = CKFetchNotificationChangesOperation(previousServerChangeToken: nil)
fnco.fetchNotificationChangesCompletionBlock = {newServerChangeToken, error in
if error == nil {
serverChangeToken = newServerChangeToken
dispatch_sync(dispatch_get_main_queue(), {
(colorCodesInUtility.subviews[10] ).hidden = false
})
} else {
Utility.writeMessageToLog("error 4559: \(error!.localizedDescription)")
}
dispatch_semaphore_signal(sema)
}
defaultContainer.addOperation(fnco)
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)
I know it is not recommended to use semaphores to control flow of the CloudKit method calls.
Do you think the last two line can be swapped? First dispatch_semaphore_wait and then addOperation be called?
Strange that app worked for iOS 8, this bug arise only in iOS 9.
Adding the following line of code will probably solve your problem:
queryOperation.qualityOfService = .UserInteractive
In iOS 9 Apple changed the behavior of that setting. Especially when using cellular data you could get the behaviour you described.
The documentation states for the .qualityOfService this:
The default value of this property is NSOperationQualityOfServiceBackground and you should leave that value in place whenever possible.
In my opinion this is more a CloudKit bug than a changed feature. More people have the same issue. I did already report it at https://bugreport.apple.com

HKStatisticsQuery Out-of-Date [duplicate]

Currently I'm trying to use HKStatisticsQuery to get the steps count between a certain time interval. I'm doing test by shaking the phone myself. However, it seems that the result I get is not the most recent one, unless:
I open the Health.app, keep it running in background, and do the test again in my app;
I open the UP app, keep it running in background, and do the test again in my app.
And if I force-quit the Health.app orUP app, my app will not be able to get the most recent data again. So UP must be doing something I'm missing, but I can't find there's any "reload" like method in HKHealthStore, or any related options in HKQuery/HKStatisticsQuery.
The code I'm using is quite straight forward as below. I wonder if there's any permissions or anything I'm missing.
let predicate = HKQuery.predicateForSamplesWithStartDate(date_start, endDate: NSDate(), options: HKQueryOptions.StrictStartDate)
var type = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
var query = HKStatisticsQuery(quantityType: type,
quantitySamplePredicate: predicate,
options: .CumulativeSum | .SeparateBySource,
completionHandler: { query, stats, error in ( /*logs here*/ ) })
let healthStore = HKHealthStore()
healthStore.executeQuery(query)
Edit: I also tried to write some data to HealthKit but the query doesn't get updated.
Edit2: when I said "most recent steps counts" I meant something like: 1. execute HKQuery; 2. shake phone; 3. execute HKQuery again. Running the code above for 2 times and I always get the same results, but if I left Health.app or the UP app in the background, the latest query got the updated results.
I also tried to call some other APIs like:
healthStore.enableBackgroundDeliveryForType(type, frequency:.Immediate, withCompletion:{
(success:Bool, error:NSError!) -> Void in
let authorized = healthStore.authorizationStatusForType(type)
LF.log("HEALTH callback success", success)
LF.log("HEALTH callback authorized", type)
})
if HKHealthStore.isHealthDataAvailable() == false {
LF.log("HEALTH data not available")
return
} else {
LF.log("HEALTH OK")
}
For almost no reason but try to secretly "trigger" some sort of background refresh. But none of these attempts worked.
HealthKit does not always have an up-to-date count of the user's steps and distance travelled. It imports these values, which actually come from CoreMotion.framework, periodically and in response to certain events. If a running application has an open HKObserverQuery or HKStatisticsCollectionQuery then HealthKit will stream the values to the client but otherwise the samples are just a snapshot from the last import.
So if you'd like to observe changes for a sample type, you should subscribe to updates using an HKObserverQuery and then query HealthKit again for the latest values. A more efficient approach would be to use HKStatisticsCollectionQuery, though, which has an update handler that will be invoked as the statistics for the samples matching the predicates change.
Finally, if you're only interested up-to-date step counts or distance travelled for at most the past 7 days then I recommend that you consider using CoreMotion.framework directly instead.

How to force a HKQuery to load the most recent steps counts?

Currently I'm trying to use HKStatisticsQuery to get the steps count between a certain time interval. I'm doing test by shaking the phone myself. However, it seems that the result I get is not the most recent one, unless:
I open the Health.app, keep it running in background, and do the test again in my app;
I open the UP app, keep it running in background, and do the test again in my app.
And if I force-quit the Health.app orUP app, my app will not be able to get the most recent data again. So UP must be doing something I'm missing, but I can't find there's any "reload" like method in HKHealthStore, or any related options in HKQuery/HKStatisticsQuery.
The code I'm using is quite straight forward as below. I wonder if there's any permissions or anything I'm missing.
let predicate = HKQuery.predicateForSamplesWithStartDate(date_start, endDate: NSDate(), options: HKQueryOptions.StrictStartDate)
var type = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
var query = HKStatisticsQuery(quantityType: type,
quantitySamplePredicate: predicate,
options: .CumulativeSum | .SeparateBySource,
completionHandler: { query, stats, error in ( /*logs here*/ ) })
let healthStore = HKHealthStore()
healthStore.executeQuery(query)
Edit: I also tried to write some data to HealthKit but the query doesn't get updated.
Edit2: when I said "most recent steps counts" I meant something like: 1. execute HKQuery; 2. shake phone; 3. execute HKQuery again. Running the code above for 2 times and I always get the same results, but if I left Health.app or the UP app in the background, the latest query got the updated results.
I also tried to call some other APIs like:
healthStore.enableBackgroundDeliveryForType(type, frequency:.Immediate, withCompletion:{
(success:Bool, error:NSError!) -> Void in
let authorized = healthStore.authorizationStatusForType(type)
LF.log("HEALTH callback success", success)
LF.log("HEALTH callback authorized", type)
})
if HKHealthStore.isHealthDataAvailable() == false {
LF.log("HEALTH data not available")
return
} else {
LF.log("HEALTH OK")
}
For almost no reason but try to secretly "trigger" some sort of background refresh. But none of these attempts worked.
HealthKit does not always have an up-to-date count of the user's steps and distance travelled. It imports these values, which actually come from CoreMotion.framework, periodically and in response to certain events. If a running application has an open HKObserverQuery or HKStatisticsCollectionQuery then HealthKit will stream the values to the client but otherwise the samples are just a snapshot from the last import.
So if you'd like to observe changes for a sample type, you should subscribe to updates using an HKObserverQuery and then query HealthKit again for the latest values. A more efficient approach would be to use HKStatisticsCollectionQuery, though, which has an update handler that will be invoked as the statistics for the samples matching the predicates change.
Finally, if you're only interested up-to-date step counts or distance travelled for at most the past 7 days then I recommend that you consider using CoreMotion.framework directly instead.

Resources