I'm trying to check some HealthKit data in the background and send a local notification if certain criteria were met.
I've added Background fetch capability and added my task's id to info.plist.
here is the background task code:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// other setups
self.setupHealthKit()
self.setupBackgroundTasks()
return true
}
fileprivate func setupHealthKit() {
HealthKitHelper.shared.executeObserverQuery(for: HealthKitTypes.activeEnergy,
predicate: nil)
.sink(receiveCompletion: {
completion in
switch completion {
case .failure(let error):
print("observer query execution failed with error: \(error.localizedDescription)")
EventLogger.BackgroundDelivery.logObserverQueryError(error: error.localizedDescription)
case .finished:
print("observer query executed.")
}
}, receiveValue: {
query, handler in
// we have some updates
EventLogger.BackgroundDelivery.logObserverQueryResultInBackground()
// check if there is a new sleep session
let queue = OperationQueue()
let operations = self.getBackgroundOperations()
let last = operations.last!
last.completionBlock = {
handler()
}
queue.addOperations(operations, waitUntilFinished: false)
})
.store(in: &self.subscriberStore)
HealthKitHelper.shared.enableBackgroundDelivery(for: HealthKitTypes.activeEnergy,
frequency: .hourly)
.sink(receiveCompletion: {
completion in
switch completion {
case .failure(let error):
print("failed to enable bg delivery with error: \(error.localizedDescription)")
case .finished:
print("delivery enable finished.")
}
}, receiveValue: {
status in
print("delivery enable success status: \(status)")
})
.store(in: &self.subscriberStore)
}
func setupBackgroundTasks() {
let status = BGTaskScheduler.shared.register(forTaskWithIdentifier: self.BACKGROUND_FETCH_TASK_ID,
using: nil) {
task in
self.handleBackgroundAppRefresh(task as! BGAppRefreshTask)
}
}
I'm just sending a local notification in my background task to see if it works.
now I'm able to run the background task in the debugger, but it never gets executed by the system.
And I also don't know what's the best way to get new health kit samples in the background.
You have to use HKObserverQuery to query health data in the background.
Related
I'm getting inconsistent results when using the Background Tasks framework for my application written in SwiftUI. I'm only looking to make quick network requests, so I'm choosing to use BGAppRefreshTask.
Background fetch, and Background Processing are set in Signing & Capabilities. Permitted background task scheduler identifiers have been set. Manually calling it in debugging works fine on a real device but never in production.
I tested both BGAppRefreshTask, and BGProcessingTask. I noticed BGProcessingTask is being called but only when connected to a power supply. I never see any updates from BGAppRefreshTask. I'm not sure if I'm missing something simple.
BGAppRefreshTask hasn't run for FOUR days now since updating this post. BGProcessingTask was run 13 time's overnight but only if my device is charging. Even when setting requiresExternalPower to false.
BGAppRefreshTask run: 0 & BGProcessingTask run: 13
Calling in the debugger using commands here works but it's never run on my device without simulating in the debugger.
(lldb) e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:#"com.bgapp.refresh"]
2022-02-26 11:41:33.964753-0800 BGAppRefreshTask[9180:2203525] Simulating launch for task with identifier com.bgapp.refresh
2022-02-26 11:41:35.908739-0800 BGAppRefreshTask[9180:2203542] Starting simulated task: <decode: missing data>
2022-02-26 11:41:35.912108-0800 BGAppRefreshTask[9180:2203542] Marking simulated task complete: <BGAppRefreshTask: com.bgapp.refresh>
Received completion: finished.
UPDATE
I used getPendingTaskRequests to see if any task was being registered and it's apparent it is but still not executing. The earliest date is scheduled for 19:28 so 7:28PM. I registered my task at 11:23AM but it's not schedule to run for another 8 hours.
Pending task requests: [<BGAppRefreshTaskRequest: com.bgapp.refresh, earliestBeginDate: 2022-02-28 19:28:34 +0000>]
BGAppRefresh
/*!
BGAppRefreshTask
#abstract A background task used to update your app's contents in the background.
*/
class BGADelegate: NSObject, UIApplicationDelegate, ObservableObject {
let taskIdentifier = "com.bgapp.refresh"
#AppStorage("backgroundtask") var tasks: Int = 0
func application(_ applicatiown: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
register()
scheduleAppRefresh()
return true
}
func register() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
print("register")
}
}
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: taskIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleAppRefresh()
task.expirationHandler = {
task.setTaskCompleted(success: false)
}
DispatchQueue.main.async {
self.tasks += 1
}
// Network request here
task.setTaskCompleted(success: true)
print("handle app refresh")
}
}
BGProcessingTask
/*!
BGProcessingTask
#abstract A background task used to perform deferrable processing.
*/
class BGPDelegate: NSObject, UIApplicationDelegate, ObservableObject {
let taskIdentifier = "com.bgapp.refresh"
#AppStorage("backgroundtask") var tasks: Int = 0
func application(_ applicatiown: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
register()
scheduleAppRefresh()
return true
}
func register() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in
self.handleAppRefresh(task: task as! BGProcessingTask)
print("register")
}
}
func scheduleAppRefresh() {
let request = BGProcessingTaskRequest(identifier: taskIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false // Default value in false
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func handleAppRefresh(task: BGProcessingTask) {
scheduleAppRefresh()
task.expirationHandler = {
task.setTaskCompleted(success: false)
}
DispatchQueue.main.async {
self.backgroundtask += 1
}
// Network request here
task.setTaskCompleted(success: true)
print("handle app refresh")
}
}
So from new understanding of Background Tasks, I know now it's being scheduled for an earliest date but I was opening the application setting back the date it's scheduled for. I was not waiting past the earlier date scheduled when relaunching the application. Each task will be overwritten when setting the background app refresh task.
struct BGAppRefreshTaskApp: App {
#UIApplicationDelegateAdaptor var delegate: AppDelegate
#Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(delegate)
.onChange(of: scenePhase) { phase in
switch phase {
case .background:
delegate.scheduleAppRefresh()
delegate.background += 1
print("background")
case .active:
print("active")
BGTaskScheduler.shared.getPendingTaskRequests(completionHandler: { request in
print("Pending task requests: \(request)")
})
case .inactive:
print("inactive")
#unknown default:
break
}
}
}
}
}
I've been successfully using BrightFutures in my apps mainly for async network requests. I decided it was time to see if I could migrate to Combine. However what I find is that when I combine two Futures using flatMap with two subscribers my second Future code block is executed twice. Here's some example code which will run directly in a playground:
import Combine
import Foundation
extension Publisher {
func showActivityIndicatorWhileWaiting(message: String) -> AnyCancellable {
let cancellable = sink(receiveCompletion: { _ in Swift.print("Hide activity indicator") }, receiveValue: { (_) in })
Swift.print("Busy: \(message)")
return cancellable
}
}
enum ServerErrors: Error {
case authenticationFailed
case noConnection
case timeout
}
func authenticate(username: String, password: String) -> Future<Bool, ServerErrors> {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}
func downloadUserInfo(username: String) -> Future<String, ServerErrors> {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> some Publisher {
return authenticate(username: username, password: password).flatMap { (isAuthenticated) -> Future<String, ServerErrors> in
guard isAuthenticated else {
return Future {$0(.failure(.authenticationFailed)) }
}
return downloadUserInfo(username: username)
}
}
let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
let cancellable2 = future.showActivityIndicatorWhileWaiting(message: "Please wait downloading")
let cancellable1 = future.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) { (output) in
print("received userInfo: '\(output)'")
}
The code simulates making two network calls and flatmaps them together as a unit which either succeeds or fails.
The resulting output is:
Calling server to authenticate
Busy: Please wait downloading
Downloading user info
Downloading user info <---- unexpected second network call
Hide activity indicator
received userInfo: 'decoded user data'
Completed without errors.
The problem is downloadUserInfo((username:) appears to be called twice. If I only have one subscriber then downloadUserInfo((username:) is only called once. I have an ugly solution that wraps the flatMap in another Future but feel I missing something simple. Any thoughts?
When you create the actual publisher with let future, append the .share operator, so that your two subscribers subscribe to a single split pipeline.
EDIT: As I've said in my comments, I'd make some other changes in your pipeline. Here's a suggested rewrite. Some of these changes are stylistic / cosmetic, as an illustration of how I write Combine code; you can take it or leave it. But other things are pretty much de rigueur. You need Deferred wrappers around your Futures to prevent premature networking (i.e. before the subscription happens). You need to store your pipeline or it will go out of existence before networking can start. I've also substituted a .handleEvents for your second subscriber, though if you use the above solution with .share you can still use a second subscriber if you really want to. This is a complete example; you can just copy and paste it right into a project.
class ViewController: UIViewController {
enum ServerError: Error {
case authenticationFailed
case noConnection
case timeout
}
var storage = Set<AnyCancellable>()
func authenticate(username: String, password: String) -> AnyPublisher<Bool, ServerError> {
Deferred {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}.eraseToAnyPublisher()
}
func downloadUserInfo(username: String) -> AnyPublisher<String, ServerError> {
Deferred {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}.eraseToAnyPublisher()
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> AnyPublisher<String, ServerError> {
let authenticate = self.authenticate(username: username, password: password)
let pipeline = authenticate.flatMap { isAuthenticated -> AnyPublisher<String, ServerError> in
if isAuthenticated {
return self.downloadUserInfo(username: username)
} else {
return Fail<String, ServerError>(error: .authenticationFailed).eraseToAnyPublisher()
}
}
return pipeline.eraseToAnyPublisher()
}
override func viewDidLoad() {
super.viewDidLoad()
authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
.handleEvents(
receiveSubscription: { _ in print("start the spinner!") },
receiveCompletion: { _ in print("stop the spinner!") }
).sink(receiveCompletion: {
switch $0 {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) {
print("received userInfo: '\($0)'")
}.store(in: &self.storage)
}
}
Output:
start the spinner!
Calling server to authenticate
Downloading user info
received userInfo: 'decoded user data'
stop the spinner!
Completed without errors.
My app uses CloudKit and I am trying to implement background fetch.
The method in App Delegate calls a method in my main view controller which checks for changes in the CloudKit database.
However, I realise that I am not calling the completion handler correctly, as the closures for the CloudKit will return asynchronously. I am really unsure how best to call the completion handler in the app delegate method once the operation is complete. Can I pass the completion handler through to the view controller method?
App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges()
completionHandler(.newData)
}
}
Main view controller method to get CloudKit changes
// Fetch zone changes (a method in main table view controller)
func getZoneChanges() {
DispatchQueue.global(qos: .userInitiated).async {
let customZone = CKRecordZone(zoneName: "Drugs")
let zoneID = customZone.zoneID
let zoneIDs = [zoneID]
let changeToken = UserDefaults.standard.serverChangeToken // Custom way of accessing User Defaults using an extension
// Look up the previous change token for each zone
var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]()
// Some other functioning code to process options
// CK Zone Changes Operation
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIDs, optionsByRecordZoneID: optionsByRecordZoneID)
// Closures for records changed, deleted etc.
// Closure details omitted for brevity as fully functional as expected.
// These closures change data model, Spotlight indexing, notifications and trigger UI refresh etc.
operation.recordChangedBlock = { (record) in
// Code...
}
operation.recordWithIDWasDeletedBlock = { (recordId, string) in
// Code...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.recordZoneFetchCompletionBlock = { (zoneId, changeToken, _, _, error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
}
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
} else {
print("Changes fetched successfully!")
// Save local items
self.saveData() // Uses NSCoding
}
}
CKContainer.default().privateCloudDatabase.add(operation)
}
}
Update your getZoneChanges to have a completion parameter.
func getZoneChanges(completion: #escaping (Bool) -> Void) {
// the rest of your code
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
completion(false)
} else {
print("Changes fetched successfully!")
completion(true)
}
}
}
Then you can update the app delegate method to use it:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges { (success) in
completionHandler(success ? .newData : .noData)
}
}
}
In case it matters, this app was a 4.2 app but is upgrading to 5.0 with new functionality, including this.
In response to a content-available APN, I need to combine local device data with remote data before triggering a message to a third party. In the foreground, this process works, but in the background, it appears to freeze until the app is in the foreground.
I thought to resolve this with a DispatchQueue -- and that is getting me a bit further , but it is still not going all the way through.
When I receive my APN, I ensure it looks right (its a content-avaialbe notification and has a category), then fire off loadPrediction:
// Tells the app that a remote notification arrived that indicates there is data to be fetched.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler:
#escaping (UIBackgroundFetchResult) -> Void) {
guard let aps = userInfo["aps"] as? [String: AnyObject] else {
completionHandler(.failed)
return
}
if aps["content-available"] as? Int == 1 {
guard let category = aps["category"] as? String else {
print("on didReceiveRemoteNotification - did not receive payload with category")
print(String(describing: userInfo))
return
}
switch category {
case APNCATEGORIES.PREDICTION.rawValue:
DataModel.shared.loadPredictions() {
completionHandler(.newData)
}
break
default:
print("on didReceiveRemoteNotification - received unknown category '\(category)'")
completionHandler(.failed)
}
} else {
print("on didReceiveRemoteNotification - did not receive content-available in APN")
print(String(describing: aps))
completionHandler(.noData)
}
}
In loadPredictions, I request two pieces of data from the backend. edit: I've read that you might want to start a different queue for each POST request, so I've revised this next code block to its current form instead of just one queue:
/** load prediction data for notification scheduling */
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let queue = DispatchQueue(label: "loadingPredictions", qos: .utility, attributes: .concurrent)
queue.sync { [weak self] in
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
self?.getPredictionsDataFromFirestore() { [weak self] in
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
let queue = DispatchQueue(label: "getNotificationSpecificationFromFireStore_\(app.name)", qos: .utility, attributes: .concurrent)
queue.async { [weak self] in
print("getting Notification Specification from FireStore")
self?.getNotificationSpecificationFromFireStore(app: app) { [weak self] spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
return
}
guard let spec = spec else {
return
}
self?.postNotification(app: app, spec: spec)
}
}
}
// loadMergedForecasts($1)
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
}
callback()
}
}
}
They don't really need to be dependently related like that, but there's no point in doing the second one if the first fails.. If they both succeed, I should post a notification to my recipient in postNotification:
/** notify third party app of available notificatiions to schedule */
func postNotification (app: App, spec: NotificationSpecification) {
print("posting notification")
do {
let notify = Data(app.notify.utf8)
let localNotificationDetails = try JSONDecoder().decode(NotificationDetails.self, from: notify)
if spec.p8 != "custom" {
let token = localNotificationDetails.token
} else {
guard let bodyJSON = localNotificationDetails.body else {
return
}
guard let url = spec.custom_endpoint else { return }
guard let methodString = spec.custom_method?.uppercased() else { return }
guard let method = HTTPMethod(rawValue:methodString) else { return }
if ![.post, .put, .patch].contains(method) {
print("app has unsupported method '\(method)' -- \(String(describing: app))")
return
}
guard var headers = spec.custom_headers else { return }
if !headers.keys.map({ entry_key in entry_key.uppercased() }).contains("CONTENT-TYPE") {
headers["Content-Type"] = "application/json"
}
print("manually posting the notification with \(String(describing: bodyJSON))")
let queue = DispatchQueue(label: "manuallyPostNotifications", qos: .utility, attributes: .concurrent)
AF.request(url, method:method, parameters: bodyJSON).responseJSON(queue: queue) { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}
}
} catch let e {
print("error posting notification to app \(app.id)\n\(e)")
}
}
NONE of these methods are on a View.
At first, there were zero cues and I dont know that I made it past the first loadPrediction. In its current state, the log looks like this when the app was in the background:
loading predictions
loading predictions - in async task, about to getPredictionsDataFromFirestore
getting Predictions Data from Firestore
for each app - about to getNotificationSpecificationFromFireStore
getting Notification Specification from FireStore
edit: that's one additional line, but it doesnt represent any improvement for the additional queues.
It will complete and succeed if I foreground it (and he whole thing takes 1-2 seconds when fully in the foreground). But I'd like to do all my work now.
Questions:
I'm doing queues wrong. How do I not exhaust the queue that I am in?
Can anyone confirm or deny that this will work when the app is closed? I can see that work is done when the app is closed, but I haven't since gone back to test if the api calls work because I cant get it to work just in the background.
addendum
revised code for current answer
/** load prediction data for notification scheduling */
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let queue = DispatchQueue(label: "loadingPredictions", qos: .default)
queue.sync { [weak self] in
let group = DispatchGroup()
group.enter()
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
self?.getPredictionsDataFromFirestore() { [weak self] in
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
group.enter()
print("getting Notification Specification from FireStore")
self?.getNotificationSpecificationFromFireStore(app: app) { [weak self] spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
group.leave()
return
}
guard let spec = spec else {
group.leave()
return
}
self?.postNotification(app: app, spec: spec) {
group.leave()
}
}
group.leave()
}
// loadMergedForecasts($1)
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
group.leave()
}
group.notify(queue: .main) {
callback()
print("I am being called too early?")
}
}
}
}
and (added a callback to the final method call):
/** notify third party app of available notificatiions to schedule */
func postNotification (app: App, spec: NotificationSpecification, _ callback: #escaping () -> Void ) {
print("posting notification")
do {
let notify = Data(app.notify.utf8)
let localNotificationDetails = try JSONDecoder().decode(NotificationDetails.self, from: notify)
if spec.p8 != "custom" {
let token = localNotificationDetails.token
callback()
} else {
guard let bodyJSON = localNotificationDetails.body else {
callback()
return
}
guard let url = spec.custom_endpoint else {
callback()
return
}
guard let methodString = spec.custom_method?.uppercased() else {
callback()
return
}
guard let method = HTTPMethod(rawValue:methodString) else {
callback()
return
}
if ![.post, .put, .patch].contains(method) {
print("app has unsupported method '\(method)' -- \(String(describing: app))")
callback()
return
}
guard var headers = spec.custom_headers else { return }
if !headers.keys.map({ entry_key in entry_key.uppercased() }).contains("CONTENT-TYPE") {
headers["Content-Type"] = "application/json"
}
print("manually posting the notification with \(String(describing: bodyJSON))")
let queue = DispatchQueue(label: "manuallyPostNotifications", qos: .utility, attributes: .concurrent)
AF.request(url, method:method, parameters: bodyJSON).responseJSON(queue: queue) { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
callback()
}
}
} catch let e {
print("error posting notification to app \(app.id)\n\(e)")
callback()
}
}
Realizing that my print statement wasn't inside the notify callback, I've revised it -- still not getting inside of the second firebase call.
loading predictions
loading predictions - in async task, about to getPredictionsDataFromFirestore
getting Predictions Data from Firestore
for each app - about to getNotificationSpecificationFromFireStore
getting Notification Specification from FireStore
I am being called too early?
You are firing off asynchronous tasks and your callback() will be executed before these tasks are complete. Since callback() eventually calls the completionHandler, your app will be suspended before all of its work is done.
You can use a dispatch group to delay the callBack() until everything is complete. The additional dispatch queues aren't necessary.
func loadPredictions(_ callback: #escaping () -> Void) {
print("loading predictions")
let dispatchGroup = DispatchGroup()
print("loading predictions - in async task, about to getPredictionsDataFromFirestore")
dispatchGroup.enter()
self.getPredictionsDataFromFirestore() {
print("getting Predictions Data from Firestore")
if let error = $2 {
NotificationCenter.default.post(Notification(name: DataModel.constants.dataFailedToLoad, object: error))
} else {
let apps = $0
apps.forEach { app in
print("for each app - about to getNotificationSpecificationFromFireStore")
dispatchGroup.enter()
self.getNotificationSpecificationFromFireStore(app: app) { spec, error in
print("got Notification Specification from FireStore, about to post notification")
if(error != nil) {
dispatchGroup.leave()
return
}
guard let spec = spec else {
dispatchGroup.leave()
return
}
self.postNotification(app: app, spec: spec)
dispatchGroup.leave()
}
}
}
NotificationCenter.default.post(Notification(name: DataModel.constants.predictionsDataLoaded))
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
callback()
}
}
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.