Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using WCSession with more than one ViewController

I found many questions and many answers but no final example for the request:

Can anyone give a final example in Objective C what is best practice to use WCSession with an IOS app and a Watch app (WatchOS2) with more than one ViewController.

What I noticed so far are the following facts:

1.) Activate the WCSession in the parent (IOS) app at the AppDelegate:

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //Any other code you might have

    if ([WCSession isSupported]) {
        self.session = [WCSession defaultSession];
        self.session.delegate = self;
        [self.session activateSession];
    }
}

2.) On the WatchOS2 side use <WCSessionDelegate>. But the rest is totally unclear for me! Some answers are talking from specifying keys in the passing Dictionary like:

[session updateApplicationContext:@{@"viewController1": @"item1"} error:&error];
[session updateApplicationContext:@{@"viewController2": @"item2"} error:&error];

Others are talking about retrieving the default session

WCSession* session = [WCSession defaultSession];
[session updateApplicationContext:applicationDict error:nil];

Others are talking about different queues? "It is the client's responsibility to dispatch to another queue if necessary. Dispatch back to the main."

I am totally confused. So please give an example how to use WCSession with an IOS app and a WatchOS2 App with more than one ViewController.

I need it for the following case (simplified): In my parent app I am measuring heart rate, workout time and calories. At the Watch app 1. ViewController I will show the heart rate and the workout time at the 2. ViewController I will show the heart rate, too and the calories burned.

like image 538
Ing. Ron Avatar asked Sep 14 '15 22:09

Ing. Ron


2 Answers

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.

like image 105
Dmytro Hutsuliak Avatar answered Nov 01 '22 00:11

Dmytro Hutsuliak


Well, this is simplified version of my solution as requested by Greg Robertson. Sorry it's not in Objective-C anymore; I'm just copy-pasting from existing AppStore-approved project to make sure there will be no mistakes.

Essentially, any WatchDataProviderDelegate can hook to data provider class as that provides array holder for delegates (instead of one weak var). Incoming WCSessionData are forwarded to all delegates using the notifyDelegates() method.

// MARK: - Data Provider Class

class WatchDataProvider: WCSessionDelegate {

    // This class is singleton
    static let sharedInstance = WatchDataProvider()

    // Sub-Delegates we'll forward to
    var delegates = [AnyObject]()

    init() {
        if WCSession.isSupported() {
            WCSession.defaultSession().delegate = self
            WCSession.defaultSession().activateSession()
            WatchDataProvider.activated = true;
        }
    }

    // MARK: - WCSessionDelegate                

    public func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
        processIncomingMessage(userInfo)
    }

    public func session(session: WCSession, didReceiveApplicationContext applicationContext: [String: AnyObject]) {
        processIncomingMessage(applicationContext)
    }

    func processIncomingMessage(dictionary: [String:AnyObject] ) {
        // do something with incoming data<
        notifyDelegates()
    }

    // MARK: - QLWatchDataProviderDelegate     

   public func addDelegate(delegate: AnyObject) {
       if !(delegates as NSArray).containsObject(delegate) {
           delegates.append(delegate)
       }
   }

   public func removeDelegate(delegate: AnyObject) {
       if (delegates as NSArray).containsObject(delegate) {
           delegates.removeAtIndex((delegates as NSArray).indexOfObject(delegate))
       }
   }

   func notifyDelegates()
   {
       for delegate in delegates {
           if delegate.respondsToSelector("watchDataDidUpdate") {
               let validDelegate = delegate as! WatchDataProviderDelegate
               validDelegate.watchDataDidUpdate()
           }
       }
   }    
}


// MARK: - Watch Glance (or any view controller) listening for changes

class GlanceController: WKInterfaceController, WatchDataProviderDelegate {

    // A var in Swift is strong by default
    var dataProvider = WatchDataProvider.sharedInstance()
    // Obj-C would be: @property (nonatomic, string) WatchDataProvider *dataProvider

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        dataProvider.addDelegate(self)
    }

    // WatchDataProviderDelegate
    func watchDataDidUpdate() {
        dispatch_async(dispatch_get_main_queue(), {
            // update UI on main thread
        })
    }}
}

class AnyOtherClass: UIViewController, WatchDataProviderDelegate {

    func viewDidLoad() {
        WatchDataProvider.sharedInstance().addDelegate(self)
    }

    // WatchDataProviderDelegate
    func watchDataDidUpdate() {
        dispatch_async(dispatch_get_main_queue(), {
            // update UI on main thread
        })
    }}
}
like image 39
igraczech Avatar answered Nov 01 '22 01:11

igraczech