Search code examples
iosiphoneswiftwatchoswcsession

How to start main iOS app from companion watch app in background?


Situation:

Since our users have updated their iOS to 11 and/or WatchOS to 4, our iOS app doesn't seem to fire any scheduled timers when the app gets started by our WatchOS app. Maybe we are doing something wrong when starting our main app from the WatchOS app.

Context & code:

Our WatchOS app is a companion app that lets the user start/stop our iPhone app in the background by pressing a button. We do this by using:

func startMainApp() {
    guard WCSession.default().isReachable == true else {
        print("Watch is not reachable")
        return
    }

    var data = [String : AnyObject]()
    data[WatchActions.actionKey()] = NSNumber.init(value: WatchActions.startApp.rawValue as Int)

    WCSession.default().sendMessage(data, replyHandler: { (result: [String : Any]) in
        let resultNumber = result[WatchActions.resultKey()] as? NSNumber
        let resultBool = resultNumber!.boolValue
        if resultBool == true {
            self.setModeActivated()
        } else {
            self.setModeActivationFailed()
        }

    }) { (error: Error) in
        if (error as NSError).code != 7012 {
            print("start app error: \(error.localizedDescription)")
            self.setModeActivationFailed()
        }
    }
}

Then in our main app, we receive the message and start our base controller:

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    if let actionNumber : NSNumber = message[WatchActions.actionKey()] as? NSNumber {
        if let watchAction : WatchActions = WatchActions(rawValue: actionNumber.intValue) {

            switch(watchAction) {
                case .isAppActive:
                    let result = BaseController.sharedInstance.sleepAndWakeUpController.isAwake()
                     replyHandler([WatchActions.resultKey() : NSNumber.init(value: result as Bool)])
                return

                case .startApp:
                    AudioController.sharedInstance().playActivatedSound()

                    let isRunningOnForeground = ApplicationStateHelper.isActive()
                    if isRunningOnForeground == false {
                        BaseController.sharedInstance.start(inBackground: true)
                    }
                    let result = true
                    replyHandler([WatchActions.resultKey() : NSNumber.init(value: result as Bool)])

                    DDLogInfo("[APPLE WATCH] [didReceiveMessage] [.startApp]")
                return
            }
        }
    }

    replyHandler([WatchActions.resultKey() : NSNumber.init(value: false as Bool)])
    return
}

Everything seems to work as before, we correctly get GPS locations, all our processes get started, however, Timer objects that get started, don't fire.

This worked perfectly before on iOS 10, so I suspect this has something to do with iOS 11 background states that work differently. However, I cannot seem to find any documentation of this.

Extra info:

  • On iOS 10, when we started our main app this way, the app got visible in the multitask view on the iPhone. Now on iOS 11, it isn't visible in the multitask view, however it does run on background. I successfully see local notifications that I schedule on the background, I can debug through the active code and when tapping the app icon, the app is immediately available.
  • Our WatchOS app has the deployment target of 2.0
  • Debugged via XCode with device connected, using Debug-->Attach to PID or Name-->Entered app name. Then start our app from the Apple Watch and debug.
  • Reproducable on iOS 11.0.3 with WatchOS 4.0 on iPhone 6

Questions: What is the best way to start our main app from the watch app? Has something changed in iOS 11/WatchOS 4 regarding to background states? Can I find documentation of this? Could this be an iOS bug?


Solution

  • All I can offer you is a confirmation that this behavior did in fact change from iOS 10 to iOS 11. It is my suspicion that the behavior on iOS 10 (and earlier?) was incorrect. Apple doesn't have any qualms about changing behavior that was inadvertent/what they deem incorrect even if developer's come to rely on the behavior (I'm pretty sure I used this behavior on my last watch project).

    The fact is that the UIApplication's state when launched by a message from the watch is background. Timer's aren't supposed to run when the application is in the background unless using particular background execution modes/background task. That fact is pretty well known and is usually encountered quite early on in an iOS developer's career. The fact that timer's would run in the background when launched from the watch was, I can surmise, a mistake.

    I do not know your use case, i.e. why you were relying on those timers, but one thing you can do that is quite simple is to create an empty background task that will get you a little more time when the app is launched.

    var backgroundTask: UIBackgroundTaskIdentifier?
    backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "app Start Task", expirationHandler: {
        guard let task = backgroundTask else { return }
        UIApplication.shared.endBackgroundTask(task)
    })
    
    let timer = Timer(timeInterval: 1, repeats: true) { (timer) in
        print("Running")
    }
    

    If you need a more consistent, longer running solution, you may need to leverage your location updates as an opportunity to do whatever work the timer is currently for. There are plenty of other background modes to pursue as well.

    Summary of your questions:

    Q: What is the best way to start our main app from the watch app?
    A: Your proposed code is a great way to launch the companion app.

    Q: Has something changed in iOS 11/WatchOS 4 regarding to background states?
    A: No, especially in regards to timers. The different behavior is likely a correction.

    Q: Can I find documentation of this?
    A: I can't. Sometimes you can squeeze this information out of apple engineers on the forums or via the code level support questions through your developer account or go to WWDC.

    Q: Could this be an iOS bug?
    A: The earlier behavior was likely the bug.