Call external function using WatchKit force touch MenuItem
Asked Answered
B

1

5

I need to implement a WatchKit force-touch MenuItem to call a saveWorkout() method that is located in a separate class that does not subclass WKInterfaceController.

I realize that every class needs at least one designated initializer. I am guessing this is the key?

Btw, my "saveSession() reached" print statement logs to the console when using the sim but not when I use a device. All other print statements log to the console even when using the device. A bit odd.

My attempts at initialization throw various errors such as:

1.fatal error: use of unimplemented initializer 'init()' for class 'DashboardController'

2.Missing argument for parameter 'context' in call

Dashboard.swift

class DashboardController: WKInterfaceController {

@IBOutlet var timerLabel: WKInterfaceTimer!
@IBOutlet weak var milesLabel: WKInterfaceLabel!

// var wSM: WorkoutSessionManager
    
//init(wSM: WorkoutSessionManager) {
//  self.wSM = wSM
//  super.init()
//  }


override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    
    addMenuItemWithItemIcon(.Accept, title: "Save", action: #selector(DashboardController.saveSession))
}

override func willActivate() {
    super.willActivate()
    print("Dashboard controller reached")
}

func saveSession() {
    //wSM.saveWorkout()
    print("saveSession() reached")    
    }

WorkoutSessionManager.swift

class WorkoutSessionContext {

let healthStore: HKHealthStore
let activityType: HKWorkoutActivityType
let locationType: HKWorkoutSessionLocationType

init(healthStore: HKHealthStore, activityType: HKWorkoutActivityType = .Other, locationType: HKWorkoutSessionLocationType = .Unknown) {
    
    self.healthStore = healthStore
    self.activityType = activityType
    self.locationType = locationType
}
}

protocol WorkoutSessionManagerDelegate: class {
// ... protocol methods
}

class WorkoutSessionManager: NSObject, HKWorkoutSessionDelegate {

let healthStore: HKHealthStore
let workoutSession: HKWorkoutSession

init(context: WorkoutSessionContext) {
    self.healthStore = context.healthStore
    self.workoutSession = HKWorkoutSession(activityType: context.activityType, locationType: context.locationType)
    self.currentActiveEnergyQuantity = HKQuantity(unit: self.energyUnit, doubleValue: 0.0)
    self.currentDistanceQuantity = HKQuantity(unit: self.distanceUnit, doubleValue: 0.0)
    
    super.init()

    self.workoutSession.delegate = self
}

func saveWorkout() {
    guard let startDate = self.workoutStartDate, endDate = self.workoutEndDate else {return}

// ...code...
Bosnia answered 2/8, 2016 at 3:16 Comment(7)
Is your saveSession() function called when you tap the menu item?Monied
Okay, let's try to get that working first. Your general approach looks fine. Double-check that Xcode is recognising your selector correctly, and try removing the class name. See also this answer for a potential issue with the simulator: https://mcmap.net/q/298057/-selector-not-called-on-selecting-menu-item-after-force-touchMonied
I have already commented out the class instantiation and method call because they didn't work in the first place. I actually saw that same link many times but wrote it off because I was testing on real device. So this time I tested again on SIM and tried the disable trick and voila I got my print statement DashboardController saveSession() reached. So for some odd reason I can get the print statement on the SIM but not on a real device. Thanks for reminding me of that link. I just upvoted that question and answer as well as added my own answer which is a little more explicit.Bosnia
No problem! It shouldn't be necessary to write a custom initializer. What happens if you uncomment those commented lines and put a print statement before the guard statement in your saveWorkout() function (in your WorkoutSessionManager class)?Monied
Well we have to back up a bit first because the reason I commented out those lines is because they don't work. Those are just my attempts. I'm pretty sure I have to implement some initialization. I'm not really worried about the sim vs device logs right now because first I need to be able to call my external method. This is the key because I need to call many external methods in my project therefore I need to know how to do that.Bosnia
Okay, the code has changed a couple of times now, and a couple of different errors are mentioned. Which error do you get when you use only your let wSM = WorkoutSessionManager() statement along with your saveSession() function?Monied
The fatal error. It compiles but I get a runtime error. Maybe WatchKit does not allow calling external functions from the extension. But somehow Apple did it in their video. They don't show all of their code.Bosnia
M
4

The fatal error is (or was) caused by this line:

let wSM = WorkoutSessionManager()

That line creates a new instance of WorkoutSessionManager and calls init() on it.

Swift provides a default initializer called init() for any structure or class that provides default values for all of its properties and does not provide at least one initializer itself. But WorkoutSessionManager does not provide default values for the healthStore and workoutSession properties (and those properties are not optionals), and it provides its own initializer named init(context:), so it has no default initializer.

You need to either create your instance of WorkoutSessionManager using the designated initializer init(context:) (passing an appropriate instance of WorkoutSessionContext) or provide a default initializer for WorkoutSessionManager named init().

The precise manner in which you should do the former depends on the implementation of the rest of your app and the presentation of your DashboardController. I assume you are trying to recreate the "Fit" app shown in WWDC 2015 Session 203.

In that demonstration, the initial controller is an instance of ActivityInterfaceController, and that controller is responsible for presenting the next interface (via segues created in the storyboard). You can see the following code in the ActivityInterfaceController class:

override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
    let activityType: HKWorkoutActivityType

    switch segueIdentifier {
    case "Running":
        activityType = .Running

    case "Walking":
        activityType = .Walking

    case "Cycling":
        activityType = .Cycling

    default:
        activityType = .Other
    }

    return WorkoutSessionContext(healthStore: self.healthStore, activityType: activityType)
}

The function above creates and returns a new instance of WorkoutSessionContext using an instance of HKHealthStore held by the initial controller. The context returned by that function is passed to the destination interface controller for the relevant segue through awakeWithContext.

For transitions in code, you can pass a context instance using equivalent functions such as pushControllerWithName(context:) which also lead to awakeWithContext.

If your initial controller is similar to the above, you can access the passed context in awakeWithContext in your DashboardController class and use it to configure a new instance of WorkoutSessionManager:

class DashboardController: WKInterfaceController
{
    // ...

    var wSM: WorkoutSessionManager?

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        if context is WorkoutSessionContext {
            wSM = WorkoutSessionManager(context: context as! WorkoutSessionContext)
        }

        addMenuItemWithItemIcon(.Accept, title: "Save", action: #selector(DashboardController.saveSession))
    }

    // ...
}

Creating an instance of WorkoutSessionManager in that way avoids calling the (non-existent) init() initializer and permits reuse of the HKHealthStore instance. Whether that approach is open to you depends on the rest of your code and the way you are presenting your DashboardController.

Note that you should avoid creating multiple instances of WorkoutSessionManager. Use a singleton to provide a single instance of WorkoutSessionManager that is shared across your extension.

Monied answered 6/8, 2016 at 10:33 Comment(4)
You might also find Apple's SpeedySloth example to be useful with respect to your broader approach: developer.apple.com/library/prerelease/content/samplecode/…Monied
Unfortunately I'm getting the error fatal error: unexpectedly found nil while unwrapping an Optional value. Why would WorkoutSessionManager be nil? When I remove ? to see what happens I get the error Class 'DashboardController' has no initializers. And then to prove that the error message about finding nil was valid I made wSM an optional ? in the saveSession() function and had no crash which verifies the compiler's original warning about nil. Btw, these are runtime errors. They occur when I press the menu item to call saveSession()Bosnia
WorkoutSessionManager will be nil where the context passed to awakeWithContext is nil or not an instance of WorkoutSessionContext. Can you post the code you are using to create and pass that context?Monied
Let us continue this discussion in chat.Monied

© 2022 - 2024 — McMap. All rights reserved.