Search code examples
iosswiftcore-datawatchkitwatchos

Core Data fetch request with WatchConnectivity


I'm currently trying to get CoreData data from my iOS app to the watchOS extension. I'm using the WatchConnectivity Framework to get a dictionary via the sendMessage(_ message: [String : Any], replyHandler: (([String : Any]) -> Void)?, errorHandler: ((Error) -> Void)? = nil) function. The basic connection is working fine. The iOS app is reachable and if I try to reply a sample dictionary everything is working.

So far so good, but as I start doing a fetch request on the iOS app in background, the Watch App never receives data. After a while I just get this error: Error while requesting data from iPhone: Error Domain=WCErrorDomain Code=7012 "Message reply took too long." UserInfo={NSLocalizedFailureReason=Reply timeout occurred., NSLocalizedDescription=Message reply took too long.}

If I open the iOS app on the iPhone and relaunch the Watch App the reply handler is getting the result. But forcing the user to actively open the iOS app on the iPhone is useless.

Can someone explain why this is happen? And what's the right way to do it? App Groups seem to be obsolete since watchOS 2.
I'm using Swift 4 btw…

On Apple Watch:

import WatchConnectivity

class HomeInterfaceController: WKInterfaceController, WCSessionDelegate {

// (…)

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {

    session.sendMessage(["request": "persons"],
                         replyHandler: { (response) in
                            print("response: \(response)")
                         },
                         errorHandler: { (error) in
                            print("Error while requesting data from iPhone: \(error)")
    })
}

On iPhone:

import CoreData
import WatchConnectivity

class ConnectivityHandler: NSObject, WCSessionDelegate {

var personsArray:[Person] = []

// (…)

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {

    // only using the next line is working!
    // replyHandler(["data": "test"])

    if message["request"] as? String == "persons" {
        fetchAllPersons()

        var allPersons: [String] = []
        for person in personsArray {
            allPersons.append(person.name!)
        }

        replyHandler(["names": allPersons])
    }
}

// this seems to be never executed (doesn't matter if it's in an extra function or right in the didReceiveMessage func)
func fetchAllPersons() {

    do {
        // Create fetch request.
        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()

        // Edit the sort key as appropriate.
        let sortDescriptor = NSSortDescriptor(key: #keyPath(Person.name), ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]

        personsArray = try DatabaseController.getContext().fetch(fetchRequest)
    } catch {
        fatalError("Failed to fetch: \(error)")
    }
}

Solution

  • After looking into this problem I found the solution by myself. The problem was that I'm using the sendMessage(_:replyHandler:errorHandler:) protocol. This is only used for transferring data when both apps are active.

    Use the sendMessage(_:replyHandler:errorHandler:) or sendMessageData(_:replyHandler:errorHandler:) method to transfer data to a reachable counterpart. These methods are intended for immediate communication between your iOS app and WatchKit extension. The isReachable property must currently be true for these methods to succeed.

    If you want to transfer data in the background you have to use updateApplicationContext(_:) or transferUserInfo(_:) depending on your needs. That's exactly what I needed!

    Use the updateApplicationContext(_:) method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. For example, an iOS app that supports Background App Refresh can use part of its background execution time to update the corresponding Watch app. This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.

    Use the transferUserInfo(_:) method to transfer a dictionary of data in the background. The dictionaries you send are queued for delivery to the counterpart and transfers continue when the current app is suspended or terminated.

    Now if the iPhone App counterpart opens the ApplicationContext or UserInfo queue is passed trough and I can add the data to my core data library.