“Message reply took too long.” - Watch Connectivit

2019-03-11 15:13发布

问题:

In my project, I use Watch Connectivity to send messages to and from the Watch and iPhone. I can send a message to the phone and receive an array of strings when launching the app, however when using actions I get the following error;

Error Domain=WCErrorDomain Code=7012 "Message reply took too long."

Here's how things are set up;

First, the watch sends a message to the phone and then the phone sends an array of strings to display in a WKInterfaceTable. This sometimes works when loading the app. ( I fetch all NSManagedObjects called Items and use their title string properties to store in an array called watchItems.

However, I have an action on the watch to delete all items in the array and refresh the table with the new data.

The action on the watch uses a sendMessage function to send the item to the phone to delete from the array, then the phone sends the newly updated array to the watch and the watch updates the table. However, I either get the same array back or an error.

Pretty simple right, so everything actually worked fine before Swift 3 and Watch OS3/iOS 10; the entire app used to work.

Here's how I have everything set up;

Phone App Delegate

import WatchConnectivity

class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {

var session : WCSession!

var items = [Items]()

func loadData() {
    let moc = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext
    let request = NSFetchRequest<Items>(entityName: "Items")

    request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
    request.predicate = NSPredicate(format: "remove == 0", "remove")

    do {
        try
            self.items = moc!.fetch(request)
        // success ...
    } catch {
        // failure
        print("Fetch failed")
    }
}

//WATCH EXTENSION FUNCTIONS
//IOS 9.3 
/** Called when the session has completed activation. If session state is WCSessionActivationStateNotActivated there will be an error with more details. */


//HAVE TO INCLUDE
@available(iOS 9.3, *)
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?){
   print("iPhone WCSession activation did complete")
}


@available(iOS 9.3, *)
func sessionDidDeactivate(_ session: WCSession) {}

func sessionWatchStateDidChange(_ session: WCSession) {}

func sessionDidBecomeInactive(_ session: WCSession) {

}

//APP DELEGATE FUNCTIONS

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {

    //Check if session is supported and Activate
    if (WCSession.isSupported()) {
        session = WCSession.default()
        session.delegate = self;
        session.activate()
    }
    return true
}


}

//DID RECIEVE MESSAGE
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Swift.Void) {


    loadData()

    func loadItems() {
        watchItems.removeAll()

        for a in self.items {
            watchItems.append(a.title)
        }
    }

    var watchItems = ["1","2","3","4","5"]

    let value = message["Value"] as? String

    //This is called when user loads app, and takes some time when using refresh action, sometimes times out 

    if value == "HELLOiPhone/+@=" {

        print("Hello Message Recieved")

        loadItems() 

        //send a reply
        replyHandler( [ "Items" : Items ] )

    }

    //Not sure if receiving but does not delete array and send back to watch
    if value == "removeALL@+=-/" {                        
        for index in self.items {
            index.remove = 1
            //Saves MOC
        }

        loadData()
        loadTasksData()

        //send a reply
        replyHandler( [ "Items" : Items ] )

    }
    else {
        for index in self.items {
            if index.title == value {
            index.remove = 1
            //Saves MOC
            }
        }

        loadData()
        loadTasksData()

        //send a reply
        replyHandler( [ "Items" : Items ] )
    }
}

WATCH

import WatchConnectivity

class SimplelistInterfaceController: WKInterfaceController, WCSessionDelegate  {


/** Called when the session has completed activation. If session state is WCSessionActivationStateNotActivated there will be an error with more details. */
@available(watchOS 2.2, *)
public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {

   //Fetch data is a function which sends a "HELLOiPhone/+@=" message to receive the array and displays in the table. This works 
   fetchData()
}


var session : WCSession!
var items = ["Refresh Items"]

override func didAppear() {
    fetchData()
}

override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
    //Check if session is supported and Activate
    if (WCSession.isSupported()) {
        session = WCSession.default()
        session.delegate = self
        session.activate()
    }
    fetchData()
}

override func awake(withContext context: Any?) {
    super.awake(withContext: context)
    fetchData()
}

@IBAction func refresh() {
    print("Refresh")
    //Works but sometimes message is delayed
    fetchData()
}

@IBAction func removeAll() {
    print("Remove All Items is called")
    if WCSession.default().isReachable {
        let messageToSend = ["Value":"removeALL@+=-/"]
        print("\(messageToSend)")
        session.sendMessage(messageToSend, replyHandler: { replyMessage in
            if let value = replyMessage["Items"] {
                self.items = value as! [String]

                Not receiving message
                print("Did Recieve Message, items = \(self.items)")
            }

            }, errorHandler: {error in
                // catch any errors here
                print(error)
        })
    }
    fetchData()
}

}

回答1:

  1. You should not send or receive custom class objects from one target(iOS) to second target(watchOS) instead you should send/receive data in dictionary format such as [String: Any] and this dictionary should contain array of your custom objects required properties in key value pair in simple dictionary. This could easily be decodable at watch side.

  2. You should make a decoupled class extending WCSessionDelegate such as below so that this class could be used not only in ExtensionDelegate but also in any WKInterfaceController.

    class WatchSessionManager: NSObject, WCSessionDelegate {
    
        static let sharedManager = WatchSessionManager()
    
        private override init() {
            super.init()
            self.startSession()
        }
    
        private let session: WCSession = WCSession.default
    
        func startSession() {
            session.delegate = self
            session.activate()
        }
    
        func tryWatchSendMessage(message: [String: Any], completion: (([String: Any]) -> Void)? = nil) {
            print("tryWatch \(message)")
            weak var weakSelf = self
            if #available(iOS 9.3, *) {
                if weakSelf?.session.activationState == .activated {
                    if weakSelf?.session.isReachable == true {
                        weakSelf?.session.sendMessage(message,
                                                      replyHandler: { [weak self]  ( response )  in
                                                        guard let slf = self else {return}
                                                        //Get the objects from response dictionary
                                                        completion?(response)
                            },
                                                      errorHandler: { [weak self] ( error )  in
                                                        guard let slf = self else {return}
                                                        print ( "Error sending message: % @ " ,  error )
                                                        // If the message failed to send, queue it up for future transfer
                                                        slf.session.transferUserInfo(message)
                        })
                    } else {
                        self.session.transferUserInfo(message)
                    }
                }else{
                    self.session.activate()
                    self.session.transferUserInfo(message)
                }
            } else {
                // Fallback on earlier versions
                if self.session.activationState == .activated {
                    if self.session.isReachable == true {
                        self.session.sendMessage(message,
                                                 replyHandler: {  ( response )  in
                                                    //Get the objects from response dictionary
                                                    completion?(response)
                        },
                                                 errorHandler: {  ( error )  in
                                                    print ( "Error sending message: % @ " ,  error )
                                                    // If the message failed to send, queue it up for future transfer
                                                    self.session.transferUserInfo(message)
                        })
                    } else {
                        self.session.transferUserInfo(message)
                    }
                }else{
                    self.session.activate()
                    self.session.transferUserInfo(message)
                }
            }
        }
    
    
    }
    

Now you could easily send a message to your iOS app to wake up and get data from there (e.g from CoreData) using the above function in any WKInterfaceController and the completion block will have your required data such as

let dict: [String: Any] = ["request": "FirstLoad"]
WatchSessionManager.sharedManager.tryWatchSendMessage(message: dict,completion:{ (data) in print(data)})

Same way you should use this WatchSessionManager on iOS side and receive the request and as per the requested key you should take data from core storage/db and send list of custom objects in simple key-value Dictionary pattern within replyHandler of didreceiveMessage function such as below.

 func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
 var dict: [String: Any] = [String: Any]()
 replyHandler(dict) //This dict will contain your resultant array to be sent to watchApp.
}

Some time iOS App(Killed state) is not reachable to WatchApp, for solving that problem you should call "tryWatchSendMessage" within Timer of around 3 sec interval. And when you get connection from watchApp then you should invalidate the timer.

The sendMessage functionality of WatchConnectivity is so powerful to wake your app up. You should use it in optimized manner.



回答2:

I have just dealt with my WatchOS app. And there was a situation when I received "Message reply took too long".

Then I added background task handler to my iOS app and started to send messages every second to WatchOS app. The message contained UIApplication.shared.backgroundTimeRemaining. So i get: 45sec, 44sec, ..., 6sec, 5sec, ... If the timer runs below 5 sec no messages will be delivered to/from iOS app and we will get "Message reply took too long". The easiest solution was to send blank messages from watch to phone each time the timer goes below 15 sec. The backgroundTimeRemaining will be updated to 45 seconds again: 45, 44, 43, ..., 17, 16, 15, (blank message), 45, 44, 43, ...

Hope it helps someone