CKQueryOperation queryCompletionBlock not called - ios

I'm using a CKQueryOperation that apparently works fine for most of my users. However, for some of them it is not working. The issue is that queryCompletionBlock is not being called.
Analysing user logs I can see that it works fine for most of the users, but it doesn't work for a few of them. It fails in all kind of iPhone models. But iOS is always iOS 14.2 on the failing devices. Unfortunately, I can not reproduce the issue on my device and this makes impossible to debug it.
I've already checked that the issue is not related with the internet connection type (wifi or data)
Any idea?
This is the code
func fetchTeams(_ success: #escaping (_ result: [CKRecord]?) -> Void,
failure: #escaping (_ error: NSError) -> Void) {
bfprint("fetchTeams starts")
let type = RecordType.Team
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: type.rawValue, predicate: predicate)
let operation = CKQueryOperation(query: query)
allTeamsRecords = []
executeQueryOperation(operation,
success: success,
failure: failure)
}
private func executeQueryOperation(_ queryOperation: CKQueryOperation,
success: #escaping (_ result: [CKRecord]?) -> Void,
failure: #escaping (_ error: NSError) -> Void) {
bfprint("executeQueryOperation starts")
let configuration = CKOperation.Configuration()
configuration.qualityOfService = .userInitiated
queryOperation.configuration = configuration
queryOperation.queuePriority = .veryHigh
queryOperation.recordFetchedBlock = { [weak self] (record) in
guard let strongSelf = self else {
bfprint("CloudKitDataProvider was deallocated before we got all team records")
return
}
strongSelf.allTeamsRecords.append(record)
}
queryOperation.queryCompletionBlock = { [weak self] (cursor, error) in
bfprint("fetch teams operation completion block called")
if let cursor = cursor {
bfprint("We got a new cursor fetching teams")
let newOperation = CKQueryOperation(cursor: cursor)
guard let strongSelf = self else {
bfprint("CloudKitDataProvider was deallocated before we got all team records")
return
}
strongSelf.executeQueryOperation(newOperation,
success: success,
failure: failure)
}
else if let error = error {
DispatchQueue.main.async(execute: {
failure(error as NSError)
bfprint("Cloud Query Error - Fetching Teams): \(error)")
})
}
else {
DispatchQueue.main.async(execute: {
bfprint("Get teams finished successfully")
guard let strongSelf = self else {
bfprint("CloudKitDataProvider was deallocated before we execute success closure")
return
}
success(strongSelf.allTeamsRecords)
})
}
}
Self.publicDB.add(queryOperation)
bfprint("query added to database")
}

I don't know what's specifically wrong in your situation, but I might offer some general guidance with CloudKit as I've worked with it over the years. CloudKit is really reliable, but it's also a little unreliable. :)
Here are some tips:
Build in mechanisms to repeatedly check that you have the latest data.
Background notifications don't always come. Have a way to get data that may have been missed.
Development and production behave a little differently in that dev seems to be a bit less reliable overall.
The CloudKit Dashboard needs to be refreshed (like the whole page in your web browser) from time-to-time because the state shown can get stale even when using the reload and query buttons in the interface.
So in your case, you might have a way to repeatedly try the CKQueryOperation so that it gets reattempted if something is haywire on CloudKit. Maintaining a local cache that syncs with CloudKit is the best way I've found to make sure your data is accurate.

I'm 99% sure it was an iOS issue. After users updated to iOS 14.3 the problem disappeared

Related

enableBackgroundDelivery Health-kit iOS15 not working correctly

I am working on the health kit. Before iOS15 below code is working correctly I am getting vitals on an hourly basis but in this latest version, I am not getting any vitals event per hour.
I am facing an issue in iOS15 not getting any event in an hourly event. Can anyone help me out? As per apple documentation https://developer.apple.com/documentation/healthkit/hkhealthstore/1614175-enablebackgrounddelivery I have enabled the permission in XCode13 still I am facing this issue.
Here is my code. (Working code before iOS15)
private func startObservingStepCountChanges() {
let sampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)
let query: HKObserverQuery = HKObserverQuery(sampleType: sampleType!, predicate: nil, updateHandler: self.stepChangeHandler)
healthKitStore.execute(query)
healthKitStore.enableBackgroundDelivery(for: sampleType!, frequency: .hourly, withCompletion: {(succeeded: Bool, error: Error!) in
if succeeded{
print("Enabled background delivery of stepcount changes")
} else {
if let theError = error{
print("Failed to enable background delivery of stepcount changes. ")
print("Error = \(theError)")
}
}
} as (Bool, Error?) -> Void)
}
private func stepChangeHandler(query: HKObserverQuery!, completionHandler: HKObserverQueryCompletionHandler!, error: Error!) {
// Flag to check the background handler is working or not
print("Backgound Mode activated")
fireTestPush()
completionHandler()
}
Check if background app refresh is enabled for your app. If it is and you're still running into this issue, file a feedback assistant report to Apple with sample code and sysdiagnose.

Combine Future Publisher is not getting deallocated

I am using the Combine Future to wrap around an async block operation and adding a subscriber to that publisher to receive the values.. I am noticing the future object is not getting deallocated, even after the subscribers are deallocated. The XCode memory graph and instruments leaks graph itself shows no reference to these future objects. I am puzzled why are they still around.
func getUsers(forceRefresh: Bool = false) -> AnyPublisher<[User], Error> {
let future = Future<[User], Error> { [weak self] promise in
guard let params = self?.params else {
promise(.failure(CustomErrors.invalidData))
return
}
self?.restApi.getUsers(params: params, forceRefresh: forceRefresh, success: { (users: [User]?, _) in
guard let users = users else {
return promise(.failure(CustomErrors.invalidData))
}
promise(.success(users))
}) { (error: Error) in
promise(.failure(error))
}
}
return future.eraseToAnyPublisher()
}
Here's how I am adding a subscription:
self.userDataService?.getUsers(forceRefresh: forceRefresh)
.sink(receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case let .failure(error) = completion {
self?.publisher.send(.error(error))
return
}
guard let users = self?.users, !users.isEmpty else {
self?.publisher.send(.empty)
return
}
self?.publisher.send(.data(users))
}) { [weak self] (response: Array<User>) in
self?.users = response
}.store(in: &self.subscribers)
deinit {
self.subscribers.removeAll()
}
This is the screenshot of the leaked memory for the future that got created above.. It's still staying around even after the subscribers are all deleted. Instruments is also showing a similar memory graph. Any thoughts on what could be causing this ??
Future invokes its closure immediately upon creation, which may be impacting this. You might try wrapping the Future in Deferred so that it isn't created until a subscription happens (which may be what you're expecting anyway from scanning the code).
The fact that it's creating one immediately is what (I think) is being reflected in the objects listed when there are no subscribers.

Firebase observer architecture

Okay, so I'm trying to build an iOS app that relies on Firebase (To work with its android version)
I started with creating a repository for each actor in my app and a general repository to manage them all
Each repository manages the observers of this actor. An example:
Inside the PagesRepository, this is a function that retrieves all the pages from Firebase and returns it inside a completionHandler:
//MARK: Gets the whole pages list
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observe(DataEventType.value) { pagesSnapshot in
guard pagesSnapshot.exists() else {
displayError(error: "Pages snapshot doesn't exist")
return
}
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}
}
And then I call it from the ViewController like this:
repository.getPagesList { (pages, error) in
guard error == nil else {
return
}
//Do processing
}
I know this may be a lot to take in, but my problem is that every time I call the function, it creates a new observer but doesn't cancel the old one... So, the completionHandler is called multiple times with different values
How should I manage this problem?
(Sorry for being complicated and a little unclear, I'm just really lost)
It seems like you only want to observe the value once so I would use this instead:
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observeSingleEvent(of: .value, with: { (pagesSnapshot) in
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}) { (error) in
// Display error
}
}

HKObserverQuery only runs when the application is reopened

So I've been following the instructions in this answer...
Healthkit background delivery when app is not running
The code runs fine and works whilst the application is open and says that background delivery is successful, however when I test the application by walking around and changing the clock on the device to an hour forward I do not receive any logs to let me know it has run. However, if I open the application again the observer query runs.
private func checkAuthorization(){
let healthDataToRead = Set(arrayLiteral: self.distanceQuantityType!)
healthKitStore.requestAuthorization(toShare: nil, read: healthDataToRead) { (success, error) in
if error != nil {
print(error?.localizedDescription)
print("There was an error requesting Authorization to use Health App")
}
if success {
print("success")
}
}
}
public func enableBackgroundDelivery() {
self.checkAuthorization()
self.healthKitStore.enableBackgroundDelivery(for: self.distanceQuantityType!, frequency: .hourly) { (success, error) in
if success{
print("Background delivery of steps. Success = \(success)")
}
if let error = error {
print("Background delivery of steps failed = \(error.localizedDescription)")
}
}
}
func observeDistance(_ handler:#escaping (_ distance: Double) -> Void) {
let updateHandler: (HKObserverQuery?, HKObserverQueryCompletionHandler?, Error?) -> Void = { query, completion, error in
if !(error != nil) {
print("got an update")
completion!()
} else {
print("observer query returned error: \(error)")
}
}
let query = HKObserverQuery(sampleType: self.distanceQuantityType!, predicate: nil, updateHandler: updateHandler)
self.healthKitStore.execute(query)
}
The query is initialised in the appDelegate method didFinishLaunching
This particular HealthKitQuery is asynchronous. You should wait until it finishes processing.
However, this case is not possible in didFinishLaunching. The application just ended execution and there is not enough time to process the query.
I would seriously suggest to rethink the logic behind the operation of your code. A good way to solve this would be to put the request elsewhere, preferrably after the needed operations were completed.

How do I "Look Me Up By Email"

I am making an iOS application and I would like to use the iCloud feature "Look Me Up By Email" found under iCloud in the Settings of iOS.
I would like to simplify the user experience by identifying users by their iCloud so they don't have to remember a login for my app.
As I understand this, this is a CloudKit feature related to CKDiscoverAllContactsOperation. You can discover people from your contacts with the same app and can be discovered by them. You need to request permission for that ability first, like this:
CKContainer.defaultContainer().requestApplicationPermission(CKApplicationPermissions.UserDiscoverability) { [unowned self] (status, error) -> Void in
//Your code handling error or success...
}
Note
Release notes for iOS 10 mention, that this operation and related functions will be changed. In iOS 10+ you need to use CKDiscoverAllUserIdentitiesOperation
Example of usage
init a container first, note that your container accountStatus must be correct
let container = CKContainer(identifier: "iCloud.com.YourContainerID")
Later, ask a permission
container.requestApplicationPermission(CKApplicationPermissions.userDiscoverability) { [unowned self] (status, error) -> Void in
if let err = error {
print(err)
}
else if status == CKApplicationPermissionStatus.granted{
//success
}
else{
print("Permission not granted")
print(status)
}
}
Later, you can get your current user record id and create a subscription for example (note that you don't need a permission and user record ID to create a subscription, but in my case it was needed to create a predicate):
container.fetchUserRecordID { [weak self] (recordID, error) in
let predicate = NSPredicate(format: "user = %#",userRecordID.recordName)
let subscription = CKSubscription(recordType: "myRecordType", predicate: predicate, options: CKSubscriptionOptions.firesOnRecordCreation)
container.publicCloudDatabase.save(subscription, completionHandler: { (subscription, error) in
//completion stuff here
}
}
Friends discoverability example
let discoverOperation = CKDiscoverAllUserIdentitiesOperation()
var users = [CKUserIdentity]()
discoverOperation.discoverAllUserIdentitiesCompletionBlock = { [weak self] (error: Error?) -> Void in
if let err = error as? NSError{
print("Discover Friends Error: \(err)")
} else {
//do whatever you want with discovered contacts
}
}
discoverOperation.userIdentityDiscoveredBlock = { (userIdentity: CKUserIdentity) -> Void in
users.append(userIdentity)
}
discoverOperation.queuePriority = Operation.QueuePriority.high //this option is up to your tasks
container.add(discoverOperation)

Resources