When to call activateSession() on WCSession object
When you put the WCSession
code in viewDidLoad
and willActivate
it is not only called when the app is opened but every time the view controller that contains the code is shown. So that is not an ideal place.
The best place to put this code is in application:didFinishLaunchingWithOptions
in your app's AppDelegate
and in applicationDidFinishLaunching
in your watch extensions's ExtensionDelegate
You can put all the session handling into a singleton class, as suggested in this great tutorial by @NatashaTheRobot.
That way the session is only created once for the time the app in being held in memory.
EDIT
As ccjensen pointed out in his comment, if you are using the connection for a Complication, Notification or Glance update you have to activate the session in the ExtensionDelegate's init
method. applicationDidFinishLaunching
will not be called in those cases.
iOS WatchOS2 WatchConnectivity [WCSession defaultSession] activateSession] failure
I Solved this issue. The code is blow.
- (void)configSession{
dispatch_async(dispatch_get_main_queue(), ^{
if (![WCSession isSupported]) {
return;
}
_session = [WCSession defaultSession];
[_session setDelegate:self];
[_session activateSession];
});
}
But I don't see any documents that need to call this method in the main thread. Moreover, this method is called before is in the main thread.So I think it might be the runloop problem.
Using WCSession with more than one ViewController
As far as I understand the task you just need synchronisation in a Phone -> Watch
direction so in a nutshell a minimum configuration for you:
Phone:
I believe the application:didFinishLaunchingWithOptions:
handler is the best place for the WCSession
initialisation therefore place the following code there:
if ([WCSession isSupported]) {
// You even don't need to set a delegate because you don't need to receive messages from Watch.
// Everything that you need is just activate a session.
[[WCSession defaultSession] activateSession];
}
Then somewhere in your code that measures a heart rate for example:
NSError *updateContextError;
BOOL isContextUpdated = [[WCSession defaultSession] updateApplicationContext:@{@"heartRate": @"90"} error:&updateContextError]
if (!isContextUpdated) {
NSLog(@"Update failed with error: %@", updateContextError);
}
update:
Watch:
ExtensionDelegate.h:
@import WatchConnectivity;
#import <WatchKit/WatchKit.h>
@interface ExtensionDelegate : NSObject <WKExtensionDelegate, WCSessionDelegate>
@end
ExtensionDelegate.m:
#import "ExtensionDelegate.h"
@implementation ExtensionDelegate
- (void)applicationDidFinishLaunching {
// Session objects are always available on Apple Watch thus there is no use in calling +WCSession.isSupported method.
[WCSession defaultSession].delegate = self;
[[WCSession defaultSession] activateSession];
}
- (void)session:(nonnull WCSession *)session didReceiveApplicationContext:(nonnull NSDictionary<NSString *,id> *)applicationContext {
NSString *heartRate = [applicationContext objectForKey:@"heartRate"];
// Compose a userInfo to pass it using postNotificationName method.
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:heartRate forKey:@"heartRate"];
// Broadcast data outside.
[[NSNotificationCenter defaultCenter] postNotificationName: @"heartRateDidUpdate" object:nil userInfo:userInfo];
}
@end
Somewhere in your Controller, let's name it XYZController1.
XYZController1:
#import "XYZController1.h"
@implementation XYZController1
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleUpdatedHeartRate:) name:@"heartRateDidUpdate" object:nil];
}
-(void)handleUpdatedHeartRate:(NSNotification *)notification {
NSDictionary* userInfo = notification.userInfo;
NSString* heartRate = userInfo[@"heartRate"];
NSLog (@"Successfully received heartRate notification!");
}
@end
Code hasn't been tested I just wrote it as is so there can be some typos.
I think the main idea now is quite clear and a transfer of remaining types of data is not that tough task.
My current WatchConnectivity architecture much more complicated but nevertheless it is based on this logic.
If you still have any questions we might move a further discussion to the chat.
Apple Watch WCConnectionDelegate, sending message in activationDidComplete fails occasionally?
In my experience watch OS is pretty finicky, especially when using older model watches. That being said I think the answer to the question: "Is it unsafe to assume the session is ready to accept communication in activationDidCompleteWith?" is yes, it is unsafe to assume that.
In my own app I have a very similar case to yours and I solved it by sending a message until a response is received.
// false until a response is received from the phone
let receivedResponse: Bool = false
// function that sends the message
func requestResponse() {
guard WCSession.default.isReachable else {
print("Phone not reachable")
return
}
// callback that handles response
let responseHandler: ([String: Any]) -> () = { response in
receivedResponse = true
callback(response)
}
WCSession.default.sendMessage(["Request": "Response"],
replyHandler: responseHandler) { error in
print(error.localizedDescription)
}
}
// timer that calls the request function repeatedly
let retryTimer = Timer.scheduledTimer(withTimeInterval: 1,
repeats: true) { timer in
if receivedResponse {
// we know we got a response so clean up timer
timer.invalidate()
}
requestResponse()
}
WCSession transferUserInfo only works in Foreground
I just watched the Watch Connectivity session from WWDC 2015. It seems that transferUserInfo
cannot be received by iOS until the app is in the foreground. That is of course what I am seeing with actual devices. The issue here then, and what has thrown me off, is that the simulator as of this writing DOES receive these messages when in the background. This is not the correct behavior and should therefore be considered a bug in the functioning of the simulator.
For my purposes I should be able to use sendMessage
from the watch to iOS when iOS is in the background. However, the same is not true in reverse. To use sendMessage
from iOS to the watch, the watch will have to be in the foreground.
Calling func session didReceiveMessage in AppDelegate
If your iPhone
app is closed and you have implemented the
func session(session: WCSession, didReceiveMessage message: [String : AnyObject]) {
}
in a specific VC you won't get the message until your VC is live but if you have implemented the above method in AppDelegate
, you will received it even when your app is closed.
As mentioned in the link
Calling this method from your WatchKit extension while it is active
and running wakes up the corresponding iOS app in the background and
makes it reachable. Calling this method from your iOS app does not
wake up the corresponding WatchKit extension
Your app did wake up in background but since your VC is not live that's why you are not getting the message.
Related Topics
How to Synchronize Coredata with Webservices in Swift
Google Sign-In via Firebase: Gidsignindelegate Does Not Conform to Viewcontroller
Saving a Codable Struct to Userdefaults with Swift
Why Are Uiscreen.Bounds Incorrect in iOS11
Argument of '#Selector' Does Not Refer to an '@Objc' Method, Property or Initializer
How to Get Frame Data in Apprtc iOS App for Video Modifications
Swift: Specialize Method of Generic Class for Function Types
Swift Repl: How to Save/Load the Repl State? (A.K.A. Suspend/Resume, Snapshot, Clone)
Classes in Swift Files Inside Folder References Not Seen by Xcode 10's Compiler
Swift: Print Name of a Function Stored in a Variable
Swift: Nstextfield Allow Only Specified Characters
How to Increase the Scope of Variables in Switch-Case/Loops in Swift
How to Disable "Save to Files" in iOS 11
Metal Kernels Not Behaving Properly on the New MACbook Pro (Late 2016) Gpus
Navigationview Bar Material Invisible on iOS 15
How to Sum the Numbers(Int16) of Stored Core Data - Swift 3
iOS Corebluetooth: Startadvertising() Error Advertising Static Data