I have got an iPhone 8.2 app that communicates with a bluetooth accessory during background mode (I enabled it on the capabilities tab). Whenever I receive a message from the accessory (handled in the iPhone app bundle) I'd like to send a notification to the Apple Watch extension (so that the user can visualise the updated state of the accessory).
How can I do this?
Additional sub-questions:
Ideally I'd like the user to see the notification also if the Apple
Watch app extension is in background mode (question 2: can apple
watch extension go in background mode?).
I am also unsure if I can send a notification without turning that
Apple watch app on. question 3: Is this possible?
You can send that notification using MMWormhole.
You send it using:
[self.wormhole passMessageObject:#{#"titleString" : title}
identifier:#"messageIdentifier"];
and you receive it using:
[self.wormhole listenForMessageWithIdentifier:#"messageIdentifier"
listener:^(id messageObject) {
// Do Something
}];
Note that wormhole uses app groups to communicate, so you need to enable it.
What MMWormhole uses, under the hood, is CFNotificationCenterGetDarwinNotifyCenter and you have more info about that in this medium post.
As for the sub-questions I am afraid I don't have 100% certain, but I believe that yes, you the apple watch extension also works in background mode. As for the third question, I didn't understand it.
An update for iOS 9.0 and above.
While MMWormhole works on watchOS 2 as well, it would be preferable to use Apple's WatchConnectivity framework on watchOS 2.0 and above. MMWormhole requires the use of App Groups, while WatchConnectivity does not. WatchConnectivity does indeed require iOS 9.0 and above.
Below is a quick example of how to send a simple String from an iOS app to a WatchKit Extension. First let's set up a helper class.
class WatchConnectivityPhoneHelper : NSObject, WCSessionDelegate
{
static let sharedInstance:WatchConnectivityPhoneHelper = WatchConnectivityPhoneHelper()
let theSession:WCSession = WCSession.defaultSession()
override init()
{
super.init()
// Set the delegate of the WCSession
theSession.delegate = self
// Activate the session so that it will start receiving delegate callbacks
theSession.activateSession()
}
// Used solely to initialize the singleton for the class, which calls the constructor and activates the event listeners
func start()
{
}
// Both apps must be active to send a message
func sendStringToWatch(message:String, callback:[String : AnyObject] -> ())
{
// Send the message to the Watch
theSession.sendMessage(["testString" : message], replyHandler:
{ (reply: [String : AnyObject]) -> Void in
// Handle the reply here
// Send the reply in the callback
callback(reply)
},
errorHandler:
{ (error:NSError) -> Void in
// Handle the error here
})
}
// A message was sent by the Watch and received by the iOS app. This does NOT handle replies to messages sent from the iOS app
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void)
{
// Handle received message logic here
if (message["testString"] != nil)
{
replyHandler(["testString" : "Received Message!"])
}
}
}
This helper class would be almost identical for the WatchKit Extension. I've named the WatchKit Extension version of this class WatchConnectivityExtensionHelper. I won't paste it because, again, it is just about identical to the helper class above.
Usage
We need to start the iOS and WatchKit Extension message listeners by instantiating the singleton helper class. All we need to do is call some function or refer to some variable within the singleton to initialize it. Otherwise, iOS or the WatchKit Extension will be sending out messages but the other might not receive them.
iOS - AppDelegate.swift
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
// Start the event listener for communications between the iOS app and the Apple Watch
WatchConnectivityPhoneHelper.sharedInstance().start()
...
}
...
}
WatchKit Extension - ExtensionDelegate.swift
class ExtensionDelegate: NSObject, WKExtensionDelegate
{
func applicationDidFinishLaunching()
{
// Start the event listener for communications between the iOS app and the Apple Watch
WatchConnectivityExtensionHelper.sharedInstance.start()
...
}
...
}
Then anywhere in your iOS app, call sendStringToWatch to send a String to the WatchKit Extension:
WatchConnectivityPhoneHelper.sharedInstance.sendStringToWatch("Test message!")
{ (reply:[String : AnyObject]) -> Void in
if (reply["testString"] != nil)
{
let receivedString:String = reply["testString"] as! String
print(receivedString)
}
}
Related
This is being tested on both Simulator and real physical device iphone5s. I tried to use WCSession sendMessage to communicate from WatchOS2 extension to iPhone iOS9 code. It works well when iphone app is running either in the foreground and background mode.
But If I kill the iPhone app (not running app at all), then I always got errorHandler timeout. So Watch cannot communicate with iPhone anymore.
"Error Domain=WCErrorDomain Code=7012 "Message reply took too long."
UserInfo={NSLocalizedDescription=Message reply took too long.,
NSLocalizedFailureReason=Reply timeout occured.}".
I think it supposed to wake iPhone app in the background.
Any idea what to work around this problem or fix it? Thank you!
It is important that you activate the WCSession in your AppDelegate didFinishLaunchingWithOptions method. Also you have to set the WCSessionDelegate there. If you do it somewhere else, the code might not be executed when the system starts the killed app in the background.
Also, you are supposed to send the reply via the replyHandler. If you try to send someway else, the system waits for a reply that never comes. Hence the timeout error.
Here is an example that wakes up the app if it is killed:
In the WatchExtension:
Setup the session. Typically in your ExtensionDelegate:
func applicationDidFinishLaunching() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
And then send the message when you need something from the app:
if WCSession.defaultSession().reachable {
let messageDict = ["message": "hello iPhone!"]
WCSession.defaultSession().sendMessage(messageDict, replyHandler: { (replyDict) -> Void in
print(replyDict)
}, errorHandler: { (error) -> Void in
print(error)
}
}
In the iPhone App:
Same session setup, but this time also set the delegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
And then implement the delegate method to send the reply to the watch:
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
replyHandler(["message": "Hello Watch!"])
}
This works whenever there is a connection between the Watch and the iPhone. If the app is not running, the system starts it in the background.
I don't know if the system waits long enough until you received your data from iCloud, but this example definitely wakes up the app.
After hours of trying and hint from #jeron. I finally figured out the problem myself.
In my session:didReceiveMessage delegate method, I have two calls. 1.replyHandler call. 2. I have an async process running (RXPromise) in my case, It nested quite a few RXPromise callbacks to fetch various data from cloud service. I didn't pay attention to it, because it is supposed to call and return right away. But now that I commented out RXPromise block all together, it can wake up iOS app in the background every time.
Finally I figure out the trouble make is because after RXPromise call, it is not guaranty to be landed back to main thread anymore. And I believe session:didReceiveMessage has to be return on the main thread. I didn't see this mentioned anywhere on the Apple documentation.
Final solution:
- (void)session:(WCSession *)session
didReceiveMessage:(NSDictionary<NSString *, id> *)message
replyHandler:(void (^)(NSDictionary<NSString *, id> *_Nonnull))replyHandler {
replyHandler(#{ #"schedule" : #"OK" });
dispatch_async(dispatch_get_main_queue(), ^{
Nested RXPromise calls.....
});
}
Well, you can use transferUserInfo in order to queue the calls. Using sendMessage will result in errors when app is killed
How can I check from watchOS 2 if application on iPhone is opened or not?
I want to send a message with NSUserDefaults from watch to iPhone via sendMessage (to be able to update interface on phone when message received) when both applications are running and I want to send NSUserDefaults even if only watchOS 2 app is running.
From what I read I found this:
/** The counterpart app must be reachable for a send message to succeed. */
#property (nonatomic, readonly, getter=isReachable) BOOL reachable;
It's always reachable from what I check.
Reachable means the apple watch and iPhone are connected via bluetooth or wifi. It doesn't necessarily mean the iPhone app is running. If reachable is true, when you try to sendMessage from the apple watch it will launch the iPhone app in the background. You need to assign the WKSession delegate as soon as possible because the delegates methods (sendMessage) will fire soon. I think what you are saying you want to do is call sendMessage if you can, and it not use the transferUserInfo method instead. To do this, first on your apple watch:
func applicationDidFinishLaunching() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
// NOTE: This should be your custom message dictionary
// You don't necessarily call the following code in
// applicationDidFinishLaunching, but it is here for
// the simplicity of the example. Call this when you want to send a message.
let message = [String:AnyObject]()
// To send your message.
// You could check reachable here, but it could change between reading the
// value and sending the message. Instead just try to send the data and if it
// fails queue it to be sent when the connection is re-established.
session.sendMessage(message, replyHandler: { (response) -> Void in
// iOS app got the message successfully
}, errorHandler: { (error) -> Void in
// iOS app failed to get message. Send it in the background
session.transferUserInfo(message)
})
}
Then, in your iOS app:
// Do this here so it is setup as early as possible so
// we don't miss any delegate method calls
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.watchKitSetup()
return true
}
func watchKitSetup() {
// Stop if watch connectivity is not supported (such as on iPad)
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
You probably want to use the application context of WatchConnectivity:
have a look at WCSession.updateApplicationContext( )
It sends the most important configuration info to the counterpart as soon as the counterpart is reachable, even if the counterpart is not reachable at the time of sending. If you call updateApplicationContext multiple times, only the latest is sent.
For much deeper info watch the WWDC 2015 session about WatchConnectivity: https://developer.apple.com/videos/wwdc/2015/?id=713
It describes more means to send data, but I think the application context fits best for you.
The session also details how to find out if the counterpart is reachable, but I think you don't need that for your use case.
I have an app that has a very rich network layer and my apple watch app depends on all the models. Unfortunately the app is not modular enough to make this layer available in the watch app.
I solved this problem by using openParentApplication: to wake up the iPhone app, perform the request and give back the results.
In watchOS 2 this method is gone and I should use WatchConnectivity. The best way to use this would be by sending userInfo dictionaries.
But how can I wake up the iPhone app to handle my requests? To get notifications about new userInfos I have to use the WCSessionDelegate and for that I need a WCSession object. But when should I create that? And how to wake up the app?
I asked an Apple Engineer about this and got the following tip: The iOS-App should be started in a background-task. So the following worked for me pretty well:
UIApplication *application = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier identifier = UIBackgroundTaskInvalid;
dispatch_block_t endBlock = ^ {
if (identifier != UIBackgroundTaskInvalid) {
[application endBackgroundTask:identifier];
}
identifier = UIBackgroundTaskInvalid;
};
identifier = [application beginBackgroundTaskWithExpirationHandler:endBlock];
Add this to your session:didReceiveMessage: or session:didReceiveMessageData: method to start a background task with a three minute timeout.
Swift version of Benjamin Herzog's suggestion below. Of note, while I did choose to push the work initiated by the Watch to a background task, as long as my app was in the background, the system woke it up just fine. Doing the work in a background task did not appear to be required, but is best practice.
override init() {
super.init()
if WCSession.isSupported() {
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let taskID = self.beginBackgroundUpdateTask()
//Do work here...
self.endBackgroundUpdateTask(taskID)
})
var replyValues = Dictionary<String, AnyObject>()
let status = "\(NSDate()): iPhone message: App received and processed a message: \(message)."
print(status)
replyValues["status"] = status
replyHandler(replyValues)
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(taskID)
}
In the WatchKit extension you will want to use the WatchConnectivity WCSession sendMessage APIs. In the extension check that the iOS app is reachable, and then send the message:
let session = WCSession.defaultSession();
if session.reachable {
let message = ["foo":"bar"]
session.sendMessage(message, replyHandler: nil, errorHandler: { (error) -> Void in
print("send failed with error \(error)")
})
}
This message will cause the system to wake the iOS app in the background, so make sure to set up the WCSession in a piece of the iOS app code that gets called when running in the background (as an example: you don't want to put it in a UIViewController's subclass's viewDidLoad) so that the message is received. Since you will be requesting some information you might want to take advantage of the reply block.
So this is how you accomplish launching the iOS app in the background, though the WatchConnectivity WWDC session recommended trying to use the "background" transfer methods if possible. If your watch app is read-only then you should be able to queue up any changes on the iOS device using the background transfers and then they will be delivered to the watch app next time it runs.
I have an iPhone application and added a WatchKitExtension. From the iPhone App I want to pass a String to the WatchApp which is supposed to change an image on the Watch.
What I already did was to download the source files and import the MMWormhole.m & .h. They are written in Obj-C and so Xcode automatically bridged them for me.
I also added an app group and activated it for my WatchExtension & my iPhone target
In the tutorial on GitHub it says I have to initialize the wormhole with:
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:#"group.com.mutualmobile.wormhole"
optionalDirectory:#"wormhole"];
...and send a message using:
[self.wormhole passMessageObject:#{#"titleString" : title}
identifier:#"messageIdentifier"];
But I have actually no idea where to put that, I am using Swift in my iPhone application and the WatchExtension.
Can anyone please help me there?
I suppose it depends on different applications, but for one application I put the listeners in the didFinishLaunchingWithOptions method of my app delegate in the main iOS app. This was because the user would be using the watch, and they would be relaying information off to the phone
There were multiple listeners...
var wormhole = MMWormhole(applicationGroupIdentifier: "group", optionalDirectory: nil)
wormhole.listenForMessageWithIdentifier("identifier", listener: { (message) -> Void in
//do stuff
})
wormhole.listenForMessageWithIdentifier("identifier2", listener: { (message) -> Void in
//do stuff
})
wormhole.listenForMessageWithIdentifier("identifier3", listener: { (message) -> Void in
//do stuff
})
And then in a WKInterfaceController, I sent a message. Sometimes in an action, sometimes in the willActivate method. It really depends on the flow of your app
var wormhole = MMWormhole(applicationGroupIdentifier: "group", optionalDirectory: nil)
#IBAction func buttonPushed(){
wormhole.passMessageObject("object", identifier: "identifier1")
}
This can work both ways though, I could have very easily put a listener in my watch which would wait for messages initiated by some Interface Controller on the phone.
Here are my instructions. Hopefully they help you with the simplest use case and then you can expand from there. (Remember to structure your code so that it actually makes sense!)
Get MMWormhole (the .h and the .m) added to your project. If you know how to use Cocoapods, do that, but otherwise, just use git submodules. (I use git submmodules)
Because you need the .h to be visible from Swift, you need to use a bridging header.
Set up an App Group, which requires using the Developer Portal. Link is here
In your iPhone build target -> Capabilities -> App Groups and add your group. If all three checkboxes do not go perfectly, go back to the Developer Portal and make sure everything is right or start again.
MMWormhole, iPhone Side
Set up the wormhole somewhere you can reach it. NOTE: your group ID has to be the one from above!
let wormhole = MMWormhole(applicationGroupIdentifier: "group.testMe.now", optionalDirectory: nil)
wormhole.listenForMessageWithIdentifier("wormholeMessageFromWatch", listener: { (message ) -> Void in
if let messageFromWatch = message as? String {
// do something with messageFromWatch
}
})
iPhone App Sends String
wormhole.passMessageObject("message from phone to watch", identifier: "wormholeMessageFromPhone")
iPhone app registers to receive and sends again in the callback via MMWormhole (asynchronous but cool)
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
universe.initWormhole(.phone, messageHandler: { (message) -> () in
universe.wormhole.passMessageObject("the phone got \(message)", identifier: "wormholeMessageFromPhone")
})
return true
}
MMWormhole, Apple Watch Side
Set up the wormhole somewhere you can reach it. NOTE: your group ID has to be the one from above!
let wormhole = MMWormhole(applicationGroupIdentifier: "group.testMe.now", optionalDirectory: nil)
wormhole.listenForMessageWithIdentifier("wormholeMessageFromPhone", listener: { (message ) -> Void in
if let messageFromPhone = message as? String {
// do something with messageFromPhone
}
})
MMWormhole, watch app registers to receive
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
universe.initWormhole(.watch, messageHandler: { (message) -> () in
println("MMWormhole Message Came to Watch: \(message)")
})
}
MMWormhole, watch app sends
// force open the parent application because otherwise the message goes nowhere until the app is opened
WKInterfaceController.openParentApplication(["":""], reply: nil)
universe.wormhole.passMessageObject("[from watch to phone]", identifier: "wormholeMessageFromWatch")
Is there any way to call a defined method in a class on the iPhone from the Watchkit extension?
From my understanding currently one of the ways to communicate locally between Watch kit and the iPhone is by using NSUserDefaults, but are there other ways?
A simple example would be great.
You can add the class to both targets (main iOS app and WatchKit extention) and use methods in WatchKit Extention directly.
Conveniently add classes (preferable utilities or categories) with few dependencies.
If file is already added to project, you can delete it (remove reference) and add it once again with including to multiple targets.
For example, my category NSString+color in project works correctly in iOS app and Watch App.
Upd: You can also do it in right panel (Utilities). See the link bellow: cs621620.vk.me/v621620973/13f86/9XYDH1HL5BI.jpg
There are two main ways to 'communicate' between your WatchKit Extension and iOS application depending on the what you are trying to accomplish.
1. openParentApplication:reply:
This will open your iOS application in the background and allow you to perform logic from your iOS code and send a response back to your Extension. For example -
[InterfaceController openParentApplication:#{ #"command": #"foo" }
reply:^(NSDictionary *replyInfo, NSError *error) {
self.item = replyInfo[#"bar"];
}];
Checkout the Framework Reference -https://developer.apple.com/library/prerelease/ios/documentation/WatchKit/Reference/WKInterfaceController_class/index.html#//apple_ref/occ/clm/WKInterfaceController/openParentApplication:reply:
MMWormhole is a library that you can use to manage these communications
2. Shared Container
If you just need access to the same data that your iOS application has access to you can implement shared containers across both targets.
This could range from just using accessing a shared NSUserDefaults, as you've mentioned or all the way up to using Core Data and accessing a shared persistence stack across both iOS and WatchKit.
Programming Reference - https://developer.apple.com/library/ios/technotes/tn2408/_index.html
3. Singleton
Perhaps you just need to access shared logic from your WatchKit Extension but don't need the complexity of the above two options. As long as you're not persisting any data across both targets you could create a Singleton class that you can call from your Extension to perform the methods you need.
According to Apple's WatchKit Programming Guide you can communicate with the iOS app on the iPhone by using openParentApplication and pass a dictionary. The parent application handles this call via handleWatchKitExtensionsRequest in its AppDelegate. From there you can call other methods depending on the passed parameters.
handleWatchKitExtensionRequest then calls the reply method to pass back parameters to the WatchKit Extension:
Watchkit Extension:
// Call the parent application from Apple Watch
// values to pass
let parentValues = [
"value1" : "Test 1",
"value2" : "Test 2"
]
WKInterfaceController.openParentApplication(parentValues, reply: { (replyValues, error) -> Void in
println(replyValues["retVal1"])
println(replyValues["retVal2"])
})
iOS App:
// in AppDelegate.swift
func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {
// retrieved parameters from Apple Watch
println(userInfo["value1"])
println(userInfo["value2"])
// pass back values to Apple Watch
var retValues = Dictionary<String,String>()
retValues["retVal1"] = "return Test 1"
retValues["retVal2"] = "return Test 2"
reply(retValues)
}
Note that this seems to be broken in Xcode 6.2 Beta 3.
For WatchOS2 openParentApplication is deprecated. The replacement for it is WCSession in Watch Connectivity Framework
First, initialize WCSession in both watch(ExtensionDelegate)& iOS(AppDelegate) with following code
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
Send a notification from watch to Phone using
session.sendMessage(msg, replyHandler: { (responses) -> Void in
print(responses)
}) { (err) -> Void in
print(err)
}
Handle the message in AppDelegate using
func session(_ session: WCSession,
didReceiveMessage message: [String : AnyObject],
replyHandler replyHandler: ([String : AnyObject]) -> Void)
{
//do something according to the message dictionary
responseMessage = ["key" : "value"] //values for the replyHandler
replyHandler(responseMessage)
}