0

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)")
    }
}
alexkaessner
  • 1,467
  • 1
  • 12
  • 28
  • Unless I'm reading it wrong, it looks like you are trying to send a message from your watch to the phone (you are using didReceiveMessage in the iPhone code) - is that what you wanted to do? – John Pollard Dec 10 '17 at 08:52
  • @JohnPollard I'm basically trying to get the CoreData to the watch. To do this I'm sending the message to the phone which should _reply_ with data in a dictionary. – alexkaessner Dec 10 '17 at 09:19

2 Answers2

3

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.

Community
  • 1
  • 1
alexkaessner
  • 1,467
  • 1
  • 12
  • 28
0

Sadly, most WatchConnectivity methods have a time limit (as told you by the error) and it seems your CoreData request is taking too much time and hence it exceeds the time limit. According to this Q&A it seems that you need to take specific precautions for doing CoreData queries in the background, so that might be the cause for your issue.

However, for best user experience I would recommend you to stop using CoreData and the WatchConnectivity framework, since the latter requires your iOS app to be running at least in the background, hence making the watchOS app dependent on the state of the iOS app and degrading the user experience on watchOS. I'd recommend you switch to Realm, since Realm supports watchOS fully and hence your watchOS app can be fully independent from your iOS app, making the user experience more fluid, since the user won't have to start the iOS app and wait for the data transmission through BLE using the WatchConnectivity framework.

Dávid Pásztor
  • 40,247
  • 8
  • 59
  • 80
  • The thing is that if I open the iOS app on my iPhone it's working. So the CoreData request seems to be fast enough. I think the problem is that it is never executed in background. And thanks for the hint with `Realm`, but this is just an update and I don't want to rework my whole app to switch to Realm. – alexkaessner Dec 10 '17 at 15:45
  • As it is explained in the linked Q&A, the CoreData fetch should work in the background as well, but its behaviour might not be the same as when it is running in the foreground, hence the exceeding time limit. – Dávid Pásztor Dec 10 '17 at 16:02