KML
KML

Reputation: 2322

Activity Indicator stops before asynchronous query ends

I have a ViewController that calls a class HKQueryWeight which runs a HealthKit Query (very, very slow btw) and saves the data to CoreData. If the user leaves the VC before the query is over, the app crashes.

fatal error: unexpectedly found nil while unwrapping an Optional value (lldb)

Initially I thought I could patch this by adding an activityIndicator which starts animating in viewDidAppear and stops at the end of the last function in the VC. It works. However, due to, I believe, the asynchronous nature of healthKit Querys, the animation stops before the actual healthKit query completes.

I am not sure if it is necessary to provide the code, but I have done so in case it is useful

ViewController:

class ViewController: UIViewController {

    @IBOutlet var activityIndicator: UIActivityIndicatorView!

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)

        activityIndicator.startAnimating()

        setupArrays ()
    }

func setupArrays (){
    println("setting up arrays")

    if NSUserDefaults.standardUserDefaults().boolForKey("hrSwitch") == true {
        var hkQueryHeartRate = HKQueryHeartRate()
        hkQueryHeartRate.performHKQuery()
    }


    if NSUserDefaults.standardUserDefaults().boolForKey("weightSwitch") == true {
        var hkQueryWeight = HKQueryWeight()
        hkQueryWeight.performHKQuery()
    }

    self.activityIndicator.stopAnimating()
}

HKQuery

import Foundation
import CoreData
import HealthKit


class HKQueryWeight: HKQueryProtocol {


    func performHKQuery() {
        var appDel:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        var context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!

        let healthKitManager = HealthKitManager.sharedInstance
        let calendar = NSCalendar.currentCalendar()

        let interval = NSDateComponents()
        interval.day = 1

        // Set the anchor date to Monday at 3:00 a.m.
        let anchorComponents =
        calendar.components(.CalendarUnitDay | .CalendarUnitMonth |
            .CalendarUnitYear | .CalendarUnitWeekday, fromDate: NSDate())

        let offset = (7 + anchorComponents.weekday - 2) % 7
        anchorComponents.day -= offset
        anchorComponents.hour = 3
        //let now = NSDate()

        let anchorDate = calendar.dateFromComponents(anchorComponents)

        let quantityType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass)

        // Create the query
        let query = HKStatisticsCollectionQuery(quantityType: quantityType,
            quantitySamplePredicate: nil,
            options: .DiscreteAverage,
            anchorDate: anchorDate,
            intervalComponents: interval)

        // Set the results handler
        query.initialResultsHandler = {
            query, results, error in

            if error != nil {
                // Perform proper error handling here
                println("*** An error occurred while calculating the statistics: \(error.localizedDescription) ***")
                abort()
            }

            let endDate = NSDate()
            let startDate =
            calendar.dateByAddingUnit(.MonthCalendarUnit,
                value: -6, toDate: endDate, options: nil)

            // Plot the weekly step counts over the past 6 months
            results.enumerateStatisticsFromDate(startDate, toDate: endDate) {
                statistics, stop in

                if let quantity = statistics.averageQuantity() {
                    let date = statistics.startDate
                    let weight = quantity.doubleValueForUnit(HKUnit.gramUnitWithMetricPrefix(.Kilo))

                    println("weight date: \(date)")
                    println("weight value: \(weight)")


                    var weightData = NSEntityDescription.insertNewObjectForEntityForName("HKWeight", inManagedObjectContext: context) as HKWeight

                    //Saving to CoreData
                    weightData.setValue(weight, forKey: "weight_data")
                    weightData.setValue(date, forKey: "weight_date")

                    context.save(nil)

                }
            }
        }

        healthKitManager.healthStore.executeQuery(query)
    }
}

Upvotes: 0

Views: 467

Answers (1)

Lyndsey Scott
Lyndsey Scott

Reputation: 37290

Right now, self.activityIndicator.stopAnimating() is being called immediately after the queries are called. Since the queries are asynchronous, they can still be being executed in the background for some time after they're called, so if you remove the activity indicator immediately after the queries are invoked, the queries probably won't be complete yet. If you want the activity indicator to stop animating after your queries are complete, you have to call for it to stop animating from within your asynchronous query block.

Since your queries are in a different class, you can post a notification to end the activity indicator's animation at the end of each query, then stop animating the UIActivityIndicatorView after the second query finishes and the second notification is received, ex:

var notificationCount:Int = 0
var totalQueries = 0

func setupArrays (){
    println("setting up arrays")

    notificationCount = 0
    totalQueries = 0
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "removeActivityIndicator", name:"ActivityIndicatorNotification", object: nil)

    if NSUserDefaults.standardUserDefaults().boolForKey("hrSwitch") == true {
        totalQueries = totalQueries + 1
        var hkQueryHeartRate = HKQueryHeartRate()
        hkQueryHeartRate.performHKQuery()
    }
    if NSUserDefaults.standardUserDefaults().boolForKey("weightSwitch") == true {
        totalQueries = totalQueries + 1
        var hkQueryWeight = HKQueryWeight()
        hkQueryWeight.performHKQuery()
    }

    if totalQueries == 0 {
        self.activityIndicator.stopAnimating()
    }
}

func removeActivityIndicator () {

    notificationCount = notificationCount + 1

    if notificationCount == totalQueries {

        dispatch_async(dispatch_get_main_queue()) {
            self.activityIndicator.stopAnimating()
            NSNotificationCenter.defaultCenter().removeObserver(self, name:"ActivityIndicatorNotification", object:nil)
        }
    }
}

Then in HKQueryWeight:

func performHKQuery() {

    // ...All the code before the query...

    // Set the results handler
    query.initialResultsHandler = {
        query, results, error in

        //...All the code currently within your query...

        NSNotificationCenter.defaultCenter().postNotificationName("ActivityIndicatorNotification", object: nil) // <-- post notification to stop animating the activity indicator once the query's complete
    }

Upvotes: 3

Related Questions