您现在的位置: 万盛学电脑网 >> 程序编程 >> 网络编程 >> ios >> 正文

App与Extensions间通信共享数据

作者:佚名    责任编辑:admin    更新时间:2022-06-22

   最近玩了玩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间通信共享数据 三联

  更多的转换规则,在上面提到的官方文档有很详细的描述,这里只说三个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”文件,