Notify WatchKit app of an update without the watch

2019-01-16 08:46发布

问题:

I'm aware of the capabilities of WKInterfaceController openParentApplication and handleWatchKitExtensionRequest methods for the watch app to open the parent app and send/receive data.

But how about this... In the instance where the user is using the parent app and performs an action in the parent app (ie changes the color of the background), how would I notify the watch app immediately and perform the relevant action on the watch also?

I believe MMWormhole would suffice in this example, is this is the best approach I should take or is there an alternative?

回答1:

Background

First off all let's sum up what we know. We have

  • app that runs on iPhone (I will refer to it as iPhone app)
  • app that runs on Watch...specifically
    • UI that runs on Watch
    • code that runs on iPhone as Extension.

First and last lines are most important to us. Yes, Extension is shipped to AppStore with your iPhone app, however this two things can run separately in iOS operating system. Hence, Extension and iPhone app are two different processes - two different programs that runs in OS.

Because of that fact, we can't use [NSNotificationCenter defaultCenter] because when you try to NSLog() defaultCenter on iPhone and defaultCenter in Extension they will have different memory address.

Darwin to the rescue!

As you might imagine, this kind of problem is not new to developers, it's proper term is Interprocess Communication. So in OS X and iOS there is...Darwin Notification mechanism. And the easiest way to use it is to implement few methods from CFNotificationCenter class.

Example

When using CFNotificationCenter you will see it looks very similar to NSNotificationCenter. My guess is NSNotif.. was built around CFNotif.. but I did't confirmed that hypothesis. Now, to the point.

So lets assume you want to send notification from iPhone to Watch back and forth. First thing we should do is to register to notifications.

- (void)registerToNotification
{    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceivedNSNotification) name:@"com.example.MyAwesomeApp" object:nil];

    CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), didReceivedDarwinNotification, CFSTR("NOTIFICATION_TO_WATCH"), NULL, CFNotificationSuspensionBehaviorDrop);
}

You probably wondering why I added observer for NSNotificationCenter? In order to accomplish our task we need to create some loop, you will see it in a moment.

As for second method.

CFNotificationCenterGetDarwinNotifyCenter() - get Darwin Notify Centre

(__bridge const void *)(self) - notification observer

didReceivedDarwinNotification - callBack method, fired when object receives notification. Basically it's the same as @selector in NSNotification

CFSTR("NOTIFICATION_TO_WATCH") - name of the notification, same story in NSNotification, but here we need CFSTR method to convert string into CFStringRef

And finally last two parameters object and suspensionBehaviour - both ignored when we are using DarwinNotifyCenter.

Cool, so we registered as an observer. So lets implement our callback methods (there are two of them, one for CFNotificationCenter, and one for NSNotificationCenter).

void didReceivedDarwinNotification()
{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"com.example.MyAwesomeApp" object:nil];
}

Now, as you see, this method doesn't starts with - (void)Name.... Why? Because it's C method. Do you see why we need NSNotificationCenter here? From C method we don't have access to self. One option is to declare static pointer to yourself, like this: static id staticSelf assign it staticSelf = self and then use it from didReceivedDarwinNotification: ((YourClass*)staticSelf)->_yourProperty but I think NSNotificationCenter is better approach.

So then in selector that responds to your NSNotification:

- (void)didReceivedNSNotification
{
    // you can do what you want, Obj-C method
}

When we are, finally, registered as observer we can send something from iPhone app.

For this we need only one line of code.

CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFSTR("NOTIFICATION_TO_WATCH"), (__bridge const void *)(self), nil, TRUE);

which can be in your ViewController, or Model.

Again, we want to get CFNotificationCenterGetDarwinNotifyCenter(), then we specify name for notification, object that is posting notification, dictionary object (ignored when using DarwinNotifyCenter and last parameters is answer to question: deliver immediately?

In similar fashion you can send notification from Watch to iPhone. From obvious reason I suggest using different notification name, like CFSTR("NOTIFICATION_TO_IPHONE") to avoid situation where, for example, iPhone sends notification to Watch and to itself.

To sum up

MMWormhole is perfectly fine and well written class, even with tests that covers most, if not all, code. It's easy to use, just remember to setup your AppGroups before. However, if you don't want to import third-party code to your project or you don't want to use it for some other reason, you can use implementation provided in this answer. Especially if you don't want to/need to exchange data between iPhone and Watch.

There is also second good project LLBSDMessaging. It's based on Berkeley sockets. More sophisticated and based on more low-level code. Here is link to lengthy but well written blog post, you will find link to Github there. http://ddeville.me/2015/02/interprocess-communication-on-ios-with-berkeley-sockets/.

Hope this help.



回答2:

I believe you might have solved your problem now. But with "watchOS 2" there's more better way without using third party classes. You can use sendMessage:replyHandler:errorHandler: method of WCSession of Watch Connectivity Class. It will work even if your iOS app is not running.

And for more info you can refer this blog.



回答3:

The answer by Ivp above is a nice one. However, I would like to add that using notifications can be tricky and I would like to share my experiences.

First, I added the observers in the method "awakeWithContext". Problem: The notifications were given several times. So, I added "removeObserver:self" before adding an observer. Problem: the observer will not be removed when "self" is different. (See also here.)

I ended up putting the following code in "willActivate":

// make sure the the observer is not added several times if this function gets called more than one time
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"com.toWatch.todo.updated" object:nil];
CFNotificationCenterRemoveObserver( CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)( self ), CFSTR( "NOTIFICATION_TO_WATCH_TODO_UPDATED" ), NULL );

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( didReceivedNSNotificationTodo ) name:@"com.toWatch.todo.updated" object:nil];
CFNotificationCenterAddObserver( CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)( self ), didReceivedDarwinNotificationTodo, CFSTR( "NOTIFICATION_TO_WATCH_TODO_UPDATED" ), NULL, CFNotificationSuspensionBehaviorDrop );

I also added the following to "didDeactivate":

[[NSNotificationCenter defaultCenter] removeObserver:self name:@"com.toWatch.todo.updated" object:nil];
CFNotificationCenterRemoveObserver( CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)( self ), CFSTR( "NOTIFICATION_TO_WATCH_TODO_UPDATED" ), NULL );

If a notification is sent to the Watch app while it is inactive, this notification is not delivered.

So, in addition to the notification mechanism above, which can inform the active Watch app of a change done on the iPhone, I use NSUserDefaults and a common app group (more info) to save information. When the controller on the Watch gets active, it checks the NSUserDefaults and updates the view if necessary.



回答4:

With WatchOS 2, you can sendMessage method like that;

Parent App

import WatchConnectivity then;

Add this to didFinishLaunchingWithOptions method in AppDelegate;

if #available(iOS 9.0, *) {
    if WCSession.isSupported() {
        let session = WCSession.defaultSession()
        session.delegate = self
        session.activateSession()

        if !session.paired {
            print("Apple Watch is not paired")
        }
        if !session.watchAppInstalled {
            print("WatchKit app is not installed")
        }
    } else {
        print("WatchConnectivity is not supported on this device")
    }
} else {
     // Fallback on earlier versions
}

Then in your notification function;

func colorChange(notification: NSNotification) {
     if #available(iOS 9.0, *) {
        if WCSession.defaultSession().reachable {
           let requestValues = ["color" : UIColor.redColor()]
           let session = WCSession.defaultSession()

           session.sendMessage(requestValues, replyHandler: { _ in
                    }, errorHandler: { error in
                        print("Error with sending message: \(error)")
                })
            } else {
                print("WCSession is not reachable to send data Watch App from iOS")
            }
     } else {
         print("Not available for iOS 9.0")
     }
 }

Watch App

Do not forget to import WatchConnectivity and add WCSessionDelegate to your InterfaceController

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)

    // Create a session, set delegate and activate it
    if (WCSession.isSupported()) {
        let session = WCSession.defaultSession()
        session.delegate = self
        session.activateSession()
    } else {
        print("Watch is not supported!")
    }
}

func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) { 
    if let deviceColor = message["color"] as? UIColor {
        // do whatever you want with color
    }
}

To do this, your Watch App need to work on foreground.