iPhone app isn't waking in the background when WCSession's sendMessage is fired

364 Views Asked by At

I'm making an app for Apple Watch that needs to wake the iPhone's counterpart app which loads a site via a WKWebView, takes a snapshot, and sends the image back.

It works perfectly when the iPhone app is on-screen, intermittently when it's running in the background, but not at all when the app is completely closed.

Is there any way to get the iPhone app to wake up in the background with WCSession's sendMessage? I've read that it's meant to but I haven't been able to get it working. Is it because the iPhone app doesn't send a reply to the initial message sent by the watch (the file that the iPhone sends back has to wait for the WKWebView to finish loading, so it can't be sent back in replyHandler)? Is there a plist setting I forgot to toggle?

The current workflow of this code is as follows:

  1. On the Apple Watch, the user taps a button which triggers the already activated WCSession's sendMessage function in ExtensionDelegate.
  2. The iPhone app receives it using the WCSession that it activated in AppDelegate.
  3. In didRecieve, the iPhone app feeds a URL into a WKWebView and starts loading it.
  4. In WKWebView's didFinish function, it takes a snapshot of the site and sends it back to the watch with transferFile.
  5. The watch receives the snapshot and passes it back to the right ViewController.

All of these steps have been tested and verified to work while both apps are on-screen, but as soon as the iPhone enters the background or has its counterpart app closed, this workflow becomes very unstable.

The relevant code is below:

  1. After the user presses the button, the ViewController fires a notification to ExtensionDelegate with the information to transmit over WCSession.

ExtensionDelegate (sending the message):

@objc func transmit(_ notification: Notification) {
        // The paired iPhone has to be connected via Bluetooth.
        if let session = session, session.isReachable {
            session.sendMessage(["SWTransmission": notification.userInfo as Any],
                replyHandler: { replyData in
                    // handle reply from iPhone app here
                    print(replyData)
                }, errorHandler: { error in
                    // catch any errors here
                    print(error)
            })
        } else {
            // when the iPhone is not connected via Bluetooth
        }
    }
  1. The iPhone app (should, but doesn't) wakes up and activates the WCSession:
fileprivate let session: WCSession? = WCSession.isSupported() ? WCSession.default : nil

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
       session?.delegate = self
       session?.activate()
       webView.navigationDelegate = self
       webView.scrollView.contentInsetAdjustmentBehavior = .never
        return true
    }
  1. The iPhone app receives the message in AppDelegate, and activates the WKWebView. Note that there isn't a configured reply. Could this be the cause of my issue?
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        
        DispatchQueue.main.async { [self] in
        let dictionary = message["SWTransmission"] as! [String: Any]
                let link = URL(string: dictionary["URL"] as! String)!
                let request = URLRequest(url: link)
                webView.frame = CGRect(x: 0, y: 0, width: Int(((dictionary["width"] as! Double) * 1.5)), height: dictionary["height"] as! Int)
                webView.load(request)
        }
    }
  1. [Still in AppDelegate] After the site is loaded, didFinish (should) gets activated, where it takes a snapshot and sends the file back to the watch via transferFile.
func getDocumentsDirectory() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            
            webView.takeSnapshot(with: nil) { [self] (image, error) in
                
                let filename = getDocumentsDirectory().appendingPathComponent("webImage.jpg")
                if let data = image!.jpegData(compressionQuality: 0.8) {
                    try? data.write(to: filename)
                }
                
                self.session?.transferFile(filename, metadata: nil)
                
            }
        }
  1. The Apple Watch receives the file in ExtensionDelegate and sends it back to the relevant ViewController:
func session(_ session: WCSession, didReceive file: WCSessionFile) {
        DispatchQueue.main.async { [self] in
            do {
                NotificationCenter.default.post(name: NSNotification.Name("openSite"), object: nil, userInfo: ["imageURL": file.fileURL] as [String: Any])
            } catch {
                print(error)
            }
        }
    }

Thank you very much for your help!

1

There are 1 best solutions below

3
On

I do not have experience working on WatchOS, but I took a look at the documentation for WCSession and it seems that what you are observing exactly matches the expected behaviour.

You mention

"It works perfectly when the iPhone app is on-screen, intermittently when it's running in the background, but not at all when the app is completely closed"

The Apple documentation for WCSession states

When both session objects are active, the two processes can communicate immediately by sending messages back and forth. When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.

These two align perfectly. When the app is on-screen, both session objects are apparently active, and as per your observation under this scenario you see the communication happening everytime. When the app is in background, the session object on the app side appears to be not active, and the transfer would take place 'opportunistically', which is in-line with you observation that the communication occurs intermittently. When the the app is completely closed, it cannot be launched by the system under any circumstance as far as I know, and this is also in-line with your observation that in-this situation the communication never happens.

Unless you've already read through the WCSession documentation, I would suggest that you go through it. I see that you are checking for WCSession's isReachable property, however, another important property that is mentioned on the documentation page is activationState. It may be worth checking the value of this property before initiating the communication.