[App与Extensions间通信共享数据]共享单车App

最近玩了玩Watch开发,而目前Watch的主要逻辑处理都是放在WatchKit Extension。真正的Host App,也就是WatchKit App只是用来在界面上显示数据的。于是实践了下containing app与app extension之间的通信和数据共享。

App Groups & Framework

这两样兵器大家都很熟悉。想要共享数据就需要开启App Groups,给group起一个风骚的名字,这样无论是NSUserDefaults还是NSFileManager都能通过App Groups共享持久层数据了。Core Data也需要NSFileManager提供存储的URL支持,而存取Core Data中的数据需要大量的模板代码,在持久层文件共享之后,代码也应该做到共享,所以将能够重用的代码打包成Framework就显得尤为重要。(除非是为了做毕设凑代码量)

还是以HardChoice为例,我新建了一个类型为Cocoa Touch Framework的target,名字叫DataKit。新建一个DataAccess.swift文件并将以前AppDelegate.swift中自动生成的Core Data模版代码转移过来。得益于Swift1.2的改进,构造一个线程安全的单例模式变得无比简单:

private static let instance = DataAccess()

public class var sharedInstance : DataAccess {

return instance

}

需要注意Swift的权限控制问题,我们需要在暴漏给框架使用者的公开接口和属性前加上public关键字修饰。

为了实现Core Data持久层共享,需要修改原先的applicationDocumentsDirectory属性:

lazy var applicationDocumentsDirectory: NSURL = {

// The directory the application uses to store the Core Data store file. This code uses a directory named "com.yxy.iCloudCoreDataTest" in the application"s documents Application Support directory.

// let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)

// return urls[urls.count-1] as! NSURL

var sharedContainerURL:NSURL? = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier(appGroupIdentifier)

return sharedContainerURL ?? NSURL()

}()

在这里containerURLForSecurityApplicationGroupIdentifier方法起到了至关作用。

同样能够共享的代码就是Model层,它们都是NSManagedObject的子类,用于存储Core Data中的数据实例。在把它们从原来的位置拖拽过来时别忘了更改下文件的target:”File inspector”->”Target Membership”,选中DataKit。

在处理iCloud与Core Data同步数据时,我对NSPersistentStoreCoordinatorStoresWillChangeNotification、NSPersistentStoreCoordinatorStoresDidChangeNotification和NSPersistentStoreDidImportUbiquitousContentChangesNotification这三个数据更新的通知进行了观察和处理,但是写在了persistentStoreCoordinator计算属性的get方法中。现在使用lazy关键字进行惰性加载,导致对这三个数据更新通知的观察延后,这会引发严重的错误。所以需要将那三个addObserverForName(name, object, queue, usingBlock)方法挪到init()方法中,在第一时间观察通知。

最后在AppDelegate.swift中添加import DataKit,替换掉中的application(application, didFinishLaunchingWithOptions) -> Bool方法中controller.managedObjectContext = managedObjectContext为controller.managedObjectContext = DataAccess.sharedInstance.managedObjectContext,也就是不再使用以前的模板代码中的上下文实例,而是用DataAccess单例中的managedObjectContext。

同理,applicationWillTerminate(application)方法中的saveContext()也要替换成DataAccess.sharedInstance.saveContext()。

于是我们也可以在App Extensions中import进来DataKit,进行地存取Core Data中的数据啦。而且用的是同一段代码,同一块数据。简直是同一个世界,同一个梦想啊。

Container app 与 Extension的通信

要知道之前做的共享数据只能是主动获取数据,并不能在数据变化时实时获取通知。如果用户在iPhone上更改了数据,我们需要在Watch上实时更改界面上数据的显示。这点NSNotificationCenter是做不到的,因为它只在App内部工作而不会在两个App之间发通知。同样KVO也无能为力,自己手写委托什么的更是别想了(因为我试过了)。直到我在这篇文章找到了救世主,问题迎刃而解:

CFNotificationCenterGetDarwinNotifyCenter

这是CoreFoundation库中一个系统级的通知中心,苹果的系统自己也在用它,看清了”Darwin”了没有?哈哈!看了下CFNotificationCenter相关的API,跟NSNotificationCenter有点像。需要用到Toll-Bridge的知识与CoreFoundation相关的类进行桥接,这虽不常用但也不难。还需要注意下个别参数的使用。

MMWormhole

更有趣的是几乎同时我也发现了MMWormhole这个开源库,它专门用于在Container app 与 Extension间传递消息。我读了下它的代码,虽然只有一个类,但是依然学到了很多。虽然在我的HardChoice上完全可以只用CFNotificationCenter进行通知就可以了,完全不需要使用MMWormhole来持久化数据和传递数据。但我觉得以后还可能会用到MMWormhole,于是我用Swift1.2重新写了一个Wormhole.swift,放在了DataKit里。

Swift与CoreFoundation

原来OC写的两百多行的MMWormhole被我用150行“清新优雅”的Swift代码取代。之所以打上引号是因为Swift与CoreFoundation之间的桥接有些不愉快。因为CoreFoundation中都是C的API,C中的指针和类型转换很出格,有安全隐患。Swift是一门安全的语言,但为了调用由历史原因造成的不安全的C的API,Swift中引入了很多类型来映射C中的类型,参考Interacting with C APIs

Swift中不用像OC那样使用__bridge和类型转换、内存管理交接,因为这些全都交给Swift了:如果Swift中存在类型映射到C的API所需的参数类型,那么可以直接将其传入API。此外内存管理也归Swift中的ARC统一管理。于是Swift大大简化了与CoreFoundation打交道的过程。

我们最关心的是指针,UnsafePointer对应了const CType *,UnsafeMutablePointer对应了CType *。当然SwiftType与CType也是对应的:

App与Extensions间通信共享数据1

更多的转换规则,在上面提到的官方文档有很详细的描述,这里只说三个tips:

在Swift中将self转成UnsafePointer(也就是const void *)只需用这个函数:unsafeAddressOf(self)

CoreFoundation库中后缀为”Ref”的类在Swift中已经去掉后缀。

Swift中函数指针被表示为CFunctionPointer,Type就是函数的类型,但还不允许你将Swift写的函数或闭包转化成CFunctionPointer,也就是干脆没提供建立CFunctionPointer实例的方法,只能通过外部引入C的函数。这就涉及到了Swift与OC混编,请戳Swift and Objective-C in the Same Project

在Framework中混编OC

我之所以需要做这种破坏工程纯洁性的事儿,是因为要用到下面这个方法来对通知进行观察:

1func CFNotificationCenterAddObserver(center: CFNotificationCenter!, observer: UnsafePointer, callBack: CFNotificationCallback, name: CFString!, object: UnsafePointer, suspensionBehavior: CFNotificationSuspensionBehavior)

除了类型为CFNotificationCallback的参数,其余的都好说:

1typealias CFNotificationCallback = CFunctionPointer Void)>

于是就回到了CFunctionPointer这块蛋疼地上了,只好在OC里写C函数然后调用之:

static NSString * const WormholeNotificationName = @"WormholeNotificationName";

@implementation HelpMethod

- (instancetype)init

{

self = [super init];

if (self) {

_callback = wormholeNotificationCallback;

}

return self;

}

void wormholeNotificationCallback(CFNotificationCenterRef center,

void * observer,

CFStringRef name,

void const * object,

CFDictionaryRef userInfo) {

NSString *identifier = (__bridge NSString *)name;

[[NSNotificationCenter defaultCenter] postNotificationName:WormholeNotificationName

object:nil

userInfo:@{@"identifier" : identifier}];

}

@end

然后在Swift中这样写就可以了:

1CFNotificationCenterAddObserver(center, unsafeAddressOf(self), helpMethod.callback, identifier, nil, CFNotificationSuspensionBehavior.DeliverImmediately)

在Swift中使用OC写的类本来是一件很easy的事儿,但是到了Framework中就变得不寻常。我在DataKit中新建了HelpMethod类,并建立”DataKit-Bridging-Header.h”文件,将HelpMethod.h头文件引入,然后在DataKit target下的”Build Settings” -> “Swift Complier-Code Generation” -> “Objective-C Bridging Header”下填入”DataKit-Bridging-Header.h”,编译出错:using bridging headers with framework targets is unsupported。

在stackoverflow上找到了解决方案,于是删除之前的”DataKit-Bridging-Header.h”文件并清除”Build Settings”关于Bridging Header的引用;在DataKit.h添加#import "HelpMethod.h",并在HelpMethod.h文件的 “File inspector”->”Target Membership”中DataKit右侧将”project”修改为”public”(否则会出现include of non-modular header inside framework module ‘DataKit’的编译错误)。

至此,我们可以在HelpMethod类中实现一个函数指针,并在Wormhole.swift文件中直接使用这个函数指针来为CFunctionPointer类型的参数传值。

总结

来个效果图:

App与Extensions间通信共享数据2

这是我第一次写Watch的App(废话谁不是第一次),经验并不是很多,也因为Swift1.2还未正式发布,遇到了一些坑。好歹最后克服了,但也丢了贞操(毕竟不是纯Swift的App了)。有不对的地方还请多多指教。随着Swift的不断完善,希望以后能够支持创建CFunctionPointer对象,这样它好我也好。