0

I have NSNotifications using Kugel that are working great on the watch simulator and both the iPhone and iPhone simulator to deliver messages to update the UI/state but these are failing to deliver on the watch side when testing on the devices.

The issue I believe is that the NSNotifications are triggered based on a WCSession message from the iPhone. Everything works fine on the simulator and iPhone side possibly because the connection and notifications are always delivered since the sim keep the watch app active all the time and the iPhone has full session support. On the watch there is the potential for failure of both the session and possibly the notification based on the state of the watch.

Debugging on the watch is painfully slow. It's taking 5-10 minutes just to start the debug process!

Can someone point me to some reading on how best to ensure a phone message is received on the watch and the watch app informed of the need to update based on a message? Or maybe some good debugging code that can log WCSession and NSNotification information that I can review later?

My code is fairly straightforward but still a work in progress ....

On both sides I create a singleton to manage the session, here is the phone side code:

import WatchConnectivity
import UIKit

// This class manages messaging between the Watch and iPhone

class PhoneSession: NSObject, WCSessionDelegate
{
     static let manager = PhoneSession()

     private var appDelegate: AppDelegate!

    private let session: WCSession? = WCSession.isSupported() ? WCSession.defaultSession() : nil

     private var validSession: WCSession?
         {
            if let session = session where session.reachable
            {
                return session
            }
            return nil
    }


    func startSession()
    {
        session?.delegate = self
        session?.activateSession()

        appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
    }


    func isEditing() -> Bool
    {
        if (UIApplication.sharedApplication().applicationState == .Active)
        {
            if (appDelegate.mainView.visible && appDelegate.mainView.currentDay.isToday())
            {
                return false
            }
            return true
        }
        return false
    }

}

extension PhoneSession
{
    func sendEditing()
    {
        if session!.reachable
        {
            sendMessage([Keys.UpdateType : PhoneUpdateType.Editing.rawValue])
        }
    }


    func sendDoneEditing()
    {
        if session!.reachable
        {
            sendMessage([Keys.UpdateType : PhoneUpdateType.DoneEdit.rawValue])
        }
    }


    func sendTable()
    {
        let tableInfo: WatchWorkout = PhoneData().buildWatchTableData()
        let archivedTable: NSData = NSKeyedArchiver.archivedDataWithRootObject(tableInfo)
        if session!.reachable
        {
            sendMessage([Keys.UpdateType : PhoneUpdateType.TableInfo.rawValue, Keys.Workout: archivedTable])
        }
        else
        {
            do
            {
                try updateApplicationContext([Keys.UpdateType : PhoneUpdateType.TableInfo.rawValue, Keys.Workout: archivedTable])
            }
            catch
            {
                print("error sending info: \(error)")
            }
        }
    }


    func sendRowDone(row: Int, done: Bool)
    {
        if session!.reachable
        {
            sendMessage([Keys.UpdateType : PhoneUpdateType.RowDone.rawValue,
                       Keys.RowIndex: row, Keys.Done: done])
        }
        else
        {
            let tableInfo: WatchWorkout = PhoneData().buildWatchTableData()
            let archivedTable: NSData = NSKeyedArchiver.archivedDataWithRootObject(tableInfo)

            do
            {
                try updateApplicationContext(   [Keys.UpdateType : PhoneUpdateType.TableInfo.rawValue,
                                            Keys.Workout: archivedTable])
            }
            catch
            {
                print("error sending info: \(error)")
            }
        }

    }


    func receivedRowDone(info: [String : AnyObject])
    {
        let row: Int = info[Keys.Row] as! Int
        let done: Bool = info[Keys.Done] as! Bool
        PhoneData.manager.updateInfoFromWatch(row, done: done)
    }


    func receivedRowInfo(info: [String : AnyObject])
    {
        let row: Int = info[Keys.Row] as! Int
        let rest: Int = info[Keys.Rest] as! Int
        let reps: Int = info[Keys.Reps] as! Int
        let force: Double = info[Keys.Force] as! Double
        PhoneData.manager.updateSetInfoFromWatch(row, rest: rest, reps: reps, force: force)
}


    func receivedTableDone(info: [String : AnyObject])
    {
        let date: Int = info[Keys.Date] as! Int
        let dones: [Bool] = info[Keys.TableDones] as! [Bool]
        PhoneData.manager.updateDones(dones, forDate: date)
        Kugel.publish(PhoneNotificationKeys.ReloadTable)
    }


    func receivedTableComplete()
    {
        Kugel.publish(PhoneNotificationKeys.ReloadTable)
    }


    func receivedStartRest()
    {
        Kugel.publish(PhoneNotificationKeys.StartRest)
    }


    func receivedInfo(info: [String : AnyObject]) -> NSData?
    {
        let messageString: String = info[Keys.UpdateType] as! String
        let updateType: WatchUpdateType = WatchUpdateType.getType(messageString)

        switch (updateType)
        {
        case .RowInfo:
            receivedRowInfo(info)
        case .TableDone:
            receivedTableDone(info)
        case .RowDone:
            receivedRowDone(info)
        case .TableComplete:
            receivedTableComplete()
        case .StartRest:
            receivedStartRest()
        case .RequestUpdate:
            let tableInfo: WatchWorkout = PhoneData().buildWatchTableData()
            let archivedTable: NSData = NSKeyedArchiver.archivedDataWithRootObject(tableInfo)
            return archivedTable
        case .Ignore:
            print("Opps")
        }
        return nil
    }

}


// MARK: Interactive Messaging
extension PhoneSession
{
    // Sender
    func sendMessage(message: [String : AnyObject], replyHandler: (([String : AnyObject]) -> Void)? = nil, errorHandler: ((NSError) -> Void)? = nil)
    {
        validSession!.sendMessage(message,
            replyHandler:
            {
                (returnMessage: [String : AnyObject]) -> Void in
                if let theMessage = returnMessage[Keys.MessageStatus]
                {
                    print("Return Message from Watch: \(theMessage)")
                }
            },
            errorHandler:
            {
                (error) -> Void in
                print("Error Message during transfer to Watch: \(error)")
            }
        )        
}


    func session(session: WCSession, didReceiveMessage message: [String : AnyObject])
    {
        self.receivedInfo(message)
    }


    // Receiver
    func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void)
    {
        let returnMessage = self.receivedInfo(message)
        if (returnMessage != nil)
        {
            if let archivedTable: NSData = returnMessage!
            {
                let replyValues = [Keys.UpdateType : PhoneUpdateType.TableInfo.rawValue, Keys.Workout: archivedTable] // Data to be returned
                replyHandler(replyValues)
            }
        }
    }

}


// MARK: Application Context
// use when your app needs only the latest information, if the data was not sent, it will be replaced
extension PhoneSession
{

    // Sender
    func updateApplicationContext(applicationContext: [String : AnyObject]) throws
     {
        if ((session) != nil)
        {
            do
            {
                try session!.updateApplicationContext(applicationContext)
            }
            catch let error
            {
                throw error
            }
        }
    }


    // Receiver
    func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject])
    {
        self.receivedInfo(applicationContext)
    }
}

and this is the watch side:

import WatchConnectivity


class WatchSession: NSObject, WCSessionDelegate
{
    static let manager = WatchSession()

    private let session: WCSession? = WCSession.isSupported() ? WCSession.defaultSession() : nil

    private var validSession: WCSession?
    {
        if let session = session where session.reachable
        {
            return session
        }
        return nil
    }

    func startSession()
    {
        session?.delegate = self
        session?.activateSession()
    }
}


extension WatchSession
{

    func sendRowInfo(row:Int, rest: Int, reps: Int, force: Double)
    {
        if session!.reachable
        {
            let message: [String: AnyObject] = [Keys.UpdateType : WatchUpdateType.RowInfo.rawValue,
                                                Keys.Row : row,
                                                Keys.Rest : rest,
                                                Keys.Reps : reps,
                                                Keys.Force : force]
            sendMessage(message)
            print("sent row done to Phone: \(message)")
        }
        else
        {
            sendTableDone()
            print("failed to connect to Phone, sent table done context to Phone")
        }
    }


    func sendRowDone(row:Int, done: Bool)
    {
        if session!.reachable
        {
            let message: [String: AnyObject] = [Keys.UpdateType : WatchUpdateType.RowDone.rawValue,
                                                Keys.Row : row,
                                                Keys.Done : done]
            sendMessage(message)
            print("sent row done to Phone: \(message)")
        }
        else
        {
            sendTableDone()
            print("failed to connect to Phone, sent table done context to Phone")
        }
    }


    func sendTableDone()
    {
        let tableDones: [Bool] = WatchData.manager.watchTableDone()
        let date: Int = WatchData.manager.date()

        do
        {
            try updateApplicationContext(   [Keys.UpdateType : WatchUpdateType.TableDone.rawValue,
                                            Keys.Date : date, Keys.TableDones: tableDones])
        }
        catch _
        {
            print("error trying to send TableDones")
        }
    }


    func sendTableComplete()
    {
        if session!.reachable
        {
            sendMessage([Keys.UpdateType : WatchUpdateType.TableComplete.rawValue])
        }
        else
        {
            let date: Int = WatchData.manager.date()

            do
            {
                try updateApplicationContext(   [Keys.UpdateType : WatchUpdateType.TableComplete.rawValue,
                                                Keys.Date : date])
            }
            catch _
            {
                print("error trying to send TableComplete")
            }
        }
    }


    func sendRest() -> Bool
    {
        var sent: Bool = false
        if session!.reachable
        {
            sendMessage([Keys.UpdateType : WatchUpdateType.StartRest.rawValue])
            sent = true
        }
        return sent
    }


    func requestUpdate() -> Bool
    {
        var sent: Bool = false
        if session!.reachable
        {
            print("requesting update reply")
            sendMessage([Keys.UpdateType : WatchUpdateType.RequestUpdate.rawValue])
            sent = true
        }
        return sent
    }


    func receivedUpdateReply(info: [String : AnyObject])
    {

    }


    func receiveRowDone(info: [String : AnyObject])
    {
        let row: Int = info[Keys.RowIndex] as! Int
        let done: Bool = info[Keys.Done] as! Bool

        WatchData.manager.updateWatchTable(row, done: done)
        Kugel.publish(WatchNotificationKeys.UpdateRow)
    }


    func receivedTable(archivedTable: NSData)
    {
        let workout: WatchWorkout = NSKeyedUnarchiver.unarchiveObjectWithData(archivedTable) as! WatchWorkout
        WatchData.manager.updateWatchWorkout(workout)

        Kugel.publish(WatchNotificationKeys.ReloadTable)
    }


    func receivedStartEditStatus()
    {
        Kugel.publish(WatchNotificationKeys.StartEdit)
    }


    func receivedDoneEditStatus()
    {
        WatchData.manager.retrieveWorkout()
        Kugel.publish(WatchNotificationKeys.DoneEdit)
    }


    func receivedStopRest()
    {
        Kugel.publish(WatchNotificationKeys.StopRest)
    }


    func receivedInfo(info: [String : AnyObject])
    {
        let messageString: String = info[Keys.UpdateType] as! String
        let updateType: PhoneUpdateType = PhoneUpdateType.getType(messageString)

        switch (updateType)
        {
            case .TableInfo:
                receivedTable(info[Keys.Workout] as! NSData)
            case .Editing:
                receivedStartEditStatus()
            case .DoneEdit:
                receivedDoneEditStatus()
            case .RowDone:
                receiveRowDone(info)
            case .StopRest:
                receivedStopRest()
            case .Ignore:
                print("Opps")
        }
    }

    func receivedReply(info: [String : AnyObject])
    {
        if let replyString: String = info[Keys.ReplyType] as? String
        {
            let replyType: ReplyType = ReplyType.getType(replyString)

            switch (replyType)
            {
            case .Table:
                print("received Reply Table")
                receivedTable(info[Keys.Workout] as! NSData)
            case .NoData:
                print("Opps ... nodata in reply")
            case .Ignore:
                print("Opps replyType message error")
            }
        }
    }
}



// MARK: Interactive Messaging
extension WatchSession
{

    // Sender    
    func sendMessage(message: [String : AnyObject], replyHandler: (([String : AnyObject]) -> Void)? = nil, errorHandler: ((NSError) -> Void)? = nil)
    {
        validSession!.sendMessage(message,
            replyHandler:
            {
                (replyMessage: [String : AnyObject]) -> Void in
                     if let typeMessage: String = replyMessage[Keys.ReplyType] as? String
                    {
                        self.receivedReply(replyMessage)
                        print("Return Message from Phone: \(typeMessage)")
                    }
            },
            errorHandler:
            {
                (error) -> Void in
                    print("Error Message during transfer to Phone: \(error)")
            }
        )
    }


    // Receiver

    func session(session: WCSession, didReceiveMessage message: [String : AnyObject])
    {
        self.receivedInfo(message)
    }


    func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void)
    {
        self.receivedInfo(message)

        let replyValues = [Keys.MessageStatus : "Watch received message"] // Data to be returned
        replyHandler(replyValues)
    }

}


// MARK: Application Context

extension WatchSession
{

    // Sender
    func updateApplicationContext(applicationContext: [String : AnyObject]) throws
    {
        if let session = validSession
        {
            do
            {
                try session.updateApplicationContext(applicationContext)
            }
            catch let error
            {
                throw error
            }
        }
    }


    // Receiver
    func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject])
    {
        // handle receiving application context
        receivedInfo(applicationContext)
    }
}

I create the singleton in my AppDelegate on the iPhone side and the ExtensionDelegate on the watch side, here is the phone side:

var phoneSession: PhoneSession!

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
    self.phoneSession = PhoneSession()
    PhoneSession.manager.startSession()

The basic logic when sending a message is it looks if the other side is reachable, if it is sendMessage is used, if it is not reachable then sendApplicationContext is used.

When the message is received on the phone side there is some extra logic to see if the app is in the foreground or background, if in foreground it will push a notification onto the main thread, if in background just the info is updated. On the watch side it always pushes onto the main thread since my understanding is messages will not be received in the background.

Greg Robertson
  • 2,317
  • 1
  • 16
  • 30
  • Which exact [WCSession methods](https://developer.apple.com/library/watchos/documentation/WatchConnectivity/Reference/WCSession_class/index.html#//apple_ref/occ/cl/WCSession) are you using to communicate from the phone to the watch? – Mike Mertsock Mar 04 '16 at 13:08
  • Added code and description of send/receive logic, let me know your thoughts – Greg Robertson Mar 05 '16 at 11:51

0 Answers0