watchOS 2 - WatchConnectivity

写在前面

虽然watchOS 2实现了可以脱离iPhone进行网络请求,但是也不可以避免的要跟iPhone进行一些数据通信。相信你对WatchConnectivity并不陌生了,只要你去Google一下就会有大把讲WatchConnectivity是怎么用的博客。但是我还是打算写一下我用下来的一些感受。当时准备开发时,也是看了很多博客的介绍,但是并没有通过看这些博客就理解透了WatchConnectivity里面的那些方法的用法和应用场景。希望这篇总结能让你少踩一些坑~。

WCSession

WatchConnectivity是一个Framework,里面包含了watch与iPhone进行数据交互的类和协议,在watch和iPhone上都支持。WCSession便是这个框架的主角,它提供了一个单例方法+ (WCSession *)defaultSession来获得当前设备上的session。在使用该session前,要先设置它的delegate,并激活它。一个设备上就只有这一个session哦,所以在使用到第三方SDK时,注意可能产生的冲突,因为delegate是一种一对一的关系。

在iPhone端配置WCSession:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (instancetype)init {
if (self = [super init]) {
if ([WCSession class]) {
if ([WCSession isSupported]) {
_session = [WCSession defaultSession];
} else {
_session = nil;
}
} else {
_session = nil;
}
}
return self;
}

- (void)startSession {
if (_session) {
_session.delegate = self;
[_session activateSession];
}
}

在watch端配置WCSession:

1
2
3
4
5
6
7
8
9
10
11
- (instancetype)init {
if (self = [super init]) {
_session = [WCSession defaultSession];
}
return self;
}

- (void)startSession {
_session.delegate = self;
[_session activateSession];
}

已经在watch端了,所以没有加上[WCSession isSupported]判断,一定是支持的。

同时,在iPhone端发送数据之前,要判断_session是否有效:

1
2
3
4
5
6
- (WCSession *)validSession {
if (_session.paired && _session.watchAppInstalled) {
return _session;
}
return nil;
}

下面将介绍几种数据传输的方式。

[_session sendMessage:replyHandler:errorHandler:]

该方法会比较及时的发送消息,如果watch端是处于活跃状态,那么在watch App中调用该个方法,会将与其配对的iPhone上的相应App在后台启动(但是你看不见哦),但是反过来在iPhone App中调用该方法是无法启动对应的watch App的。在掉用该方法时一定要判断session的可到达性_session.reachable。关于这个到达心,可以看一下官方的解释:

在天气王App中,用户一打开手表时,就要知道有在手机上添加了几个城市,又由于用户添加的城市列表没有同步到服务器上,所以只能通过iPhone来拿数据,此处就用到了该方法。其实就是用watch给iPhone发送一条拿城市的信息,iPhone收到该信息后,把城市列表数据再发给watch。发送的消息就是一个固定的key-value:

1
2
3
4
5
6
7
8
9
- (NSDictionary *)getCitysMessage {
//获取系统当前的时间戳
NSDate* dat = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval a=[dat timeIntervalSince1970]*1000;
NSString *timeString = [NSString stringWithFormat:@"%f", a];
//发送的消息与上次一样则不会发送,因此加上时间戳
NSDictionary *dict = @{@"watchSessionSycnCitys":[NSString stringWithFormat:@"%@%@", @"watchSessionSycnCitys", timeString]};
return dict;
}

对于同样的数据内容,WCSession不会连续发送2次,所以加上一个时间戳。

使用-(void)sendMessage:replyHandler:errorHandler:有2种方式来接收iPhone传过来的数据:

  • 在iPhone端实现WCSessionDelegate中的-(void)session:didReceiveMessage:replyHandler:方法,然后把数据交给replyHandler。在watch端直接在-(void)sendMessage:replyHandler:errorHandler:中的replyHandler中处理收到的数据就可以了。
  • 在iPhone端同样实现WCSessionDelegate中的-(void)session:didReceiveMessage:replyHandler:方法,然后调用sendMessage把数据发送出去。在watch端也实现这个delegate方法,然后处理receiveMessage就可以了。

[_session updateApplicationContext:error:]

该方法会把最近的一次数据发送给对方。也就是说,如果我用它发出去了一次数据,对方没有处理,然后我又用它发了一次数据,那么这次的数据会把上次的给覆盖掉,也就是上一次的数据对方永远收不到了。此外,如果本次发送的数据是与上次一模一样的,那么本次的数据就不会发送了。它发送消息不是及时的。

在天气王App中,用户每次在手机上添加、删除城市,或者设置了默认城市,需要同步到手表上,但是又不需要及时处理,只要在用户打开手表时,看到的城市与手机上的一致就行了。在用户每次添加了城市,或删除了城市,或设置了默认城市,就会把这个城市列表发送一次。

在接收数据的一端,实现WCSessionDelegate中的- (void)session:didReceiveApplicationContext:方法,如果需要更新界面,注意要在主线程中来处理收到的数据,这个delegate方法不保证一定是在主线程中执行的。

有一点想特意说一下,一定要把updateApplicationContext中的error用弹窗形式打印出来,我就掉到这个坑里了,只用了NSLog,但是调试Watch端的时候,iPhone中的信息是不会打印到控制台上的…,于是在收不到数据时就一脸懵bi了。

1
2
3
4
5
6
7
8
9
10
- (void)updateApplicationContext:(NSDictionary *)applicationContext {
if ([self validSession]) {
NSError *error = nil;
[_session updateApplicationContext:applicationContext error:&error];
if (error) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"error" message:error.description delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];
[alert show];
}
}
}

[_session transferUserInfo]

该方法会把数据放到队列中,然后按照顺序发送出去。一旦数据开始传输了,即使app被挂起,该传输过程也不会被中断。同时注意,该方法是有返回值的,返回了WCSessionUserInfoTransfer,让你可以取消本次数据传输,查看传输的数据,和是否正在进行传输。

在天气王App中,watch端和iPhone端拉取天气数据的接口都是一样了,为了避免浪费请求,在iPhone拿到天气数据后,会通过该方法将其同步到watch上。

WCSessionDelegate的代理方法sessionReachabilityDidChange

这个delegate方法会在对方的session可到达性发生变化时被触发。比如你某一天出门时忘记戴手表了,然后白天时iPhone天气王App中添加了几个城市,晚上下班回到家,这时拿起手表看天气,需要立即同步到白天添加的城市,就可以在该方法中处理了。

WCSession在代码层的设计

简单的说一下我的做法,作为你去处理这块的一个参考,有什么好的建议也希望在留言中分享哦~。

WCSession在iOS App上和WatchKit Extension上都只能有一个,所以用一个单例类来管理对它的一些操作还是比较合理的,在iOS App上和WatchKit Extension上都分别创建一个WatchSessionManager类。这个类中的_session最好不要暴露出去,又因为涉及到的操作也不是很多,所以我把具体的业务处理也放在了这个类中,然后直接提供封装好的具体方法出去,比如getCitysFromIphone这样。

在watch端拿到数据后,还要通知界面做相应的处理,用NSNotification显然会把代码弄的非常不优雅,于是用delegate的方式了。WatchSessionManager要通知的对象通常不会只有一个,所以这里我用了一个:

1
@property (nonatomic, strong) NSPointerArray *weakRefDelegates;

来放所有的代理对象。为了方便使用,封装了以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)addDelegate:(id)delegate {

if (!_weakRefDelegates) {
_weakRefDelegates = [NSPointerArray weakObjectsPointerArray];
}

if (![_weakRefDelegates containsObject:delegate]) {
[_weakRefDelegates addObject:delegate];
}
}

- (void)removeDelegate:(id)delegate {
[_weakRefDelegates removeObject:delegate];
}

- (void)notifyDelegates:(SEL)selector withObject:(id)param {
if (!_weakRefDelegates) {
return;
}

for (int i= 0; i < [_weakRefDelegates count]; i++) {
id obj = [_weakRefDelegates objectAtIndex:i];
if (obj && [obj respondsToSelector:selector]) {
IMP imp = [obj methodForSelector:selector];
void (*func)(id, SEL, id, id) = (void (*) (id, SEL, id, id))imp;
func(obj, selector, self, param);
}
}
}

其中用到的NSPointerArray的removeObject等方法是自己写的它的一个category

至此,总结完毕,欢迎拍砖^_^

–End–

以上如果有说的不严谨的地方,希望在下面的留言中指出哦,如果对你有一些帮助,也希望能在下面的留言中给一些鼓励^_^