从iOS 8起,就有了App Extension。Extension的种类至今也扩充到了19种,应用也很广泛,值得重点关注起来。
Extension几乎可以看做一个内嵌的独立App,拥有独立的BundleID、证书、概要配置文件、进程空间、沙盒等等。只是需要打包在App内,类似于寄生在宿主App内,捆绑安装。不过一旦安装应用后,扩展可以由系统独立调用,执行扩展内的代码逻辑。
这篇只记录之前开发Today Extension的过程中,值得注意的事项。其实大多都是通用的。
1.创建Extension和证书管理
在已创建的项目中,新建File->New->Target->Today Extension,确定名称后,就可以在工程文件的Targets列表中,看到新建的extension。
在General栏可以看到,Extension有独立的Identity内容,Bundle Identifier一般具有宿主应用的Bundle Identifier前缀。如果使用Xcode8的自动管理证书,并登录了开发者账号,可以在开发者中心看到自动创建的该AppID。
接着为该AppID创建Provisioning files即可。
2.MainInterface.storyboard和主要的类
新创建的Extension与App的主Target分别在不同的文件目录下,彼此隔离。主界面、Info.plist和本地化文件等都是单独管理的,所以可创建并修改InfoPlist.strings的本地化文件中CFBundleDisplayName字段,单独为扩展命名,如果不修改,将默认使用宿主App的显示名称。
扩展和宿主App是隔离的,不能使用彼此的类、框架、资源,如果想使用宿主的类库或者资源文件等,最直接的办法就是拷贝到当前Target或者增加需要Link的框架。不过,查看Extension的Build Settings,Prefix Header可以填写宿主App的Prefix Header,并使用其引用头文件中的宏定义。
扩展默认使用了MainInterface.storyboard,并绑定了一个类TodayViewController,并已经创建了一个Hello World视图,我们可以直接使用并修改。但是如果不想使用storyboard或者需要修改启动类,可以将Info.plist文件中NSExtension字典的NSExtensionMainInterface项去掉,增加NSExtensionPrincipalClass项,value则为指定启动类的类名,例如TodayViewController。
3.生命周期
扩展的视图控制器与宿主的视图控制器一样,具有常见的生命周期方法。例如当第一次在下拉的“今日”列表中显示本应用扩展,则会执行viewDidLoad方法,然后依次执行其他方法;消失和再次出现,则会调用disappear和appear系列方法;即将被销毁时候,调用dealloc方法。
4.Today Extension的size
iOS10以后,重新规定了Today Extension的size。宽度是固定(例如在iPhone6上是359),所以无法改变;但是高度方面,提供了两种模式:
NCWidgetDisplayModeCompact:固定高度,则为110
NCWidgetDisplayModeExpanded:可以变化的高度,区间为110~616
使用如下代码可以修改模式:
if ([[UIDevice currentDevice].systemVersion floatValue] >= 10) {
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}
我们可以在NCWidgetProviding协议的如下代理方法中,取到size的最大范围:
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize NS_AVAILABLE_IOS(10_0);
用如下方法确定边距:
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets NS_DEPRECATED_IOS(8_0, 10_0, "This method will not be called on widgets linked against iOS versions 10.0 and later.");
如果需要修改Widget展示的背景高度,可以修改属性:self.preferredContentSize
5.数据共享
既然扩展与宿主App是隔离的,那么数据共享就需要使用App Groups了。
在App主Target的Capabilities栏,找到App Groups项,开启功能,并点击“+”符号添加一个共享的数据容器名称,例如group.xxx。然后会发现主Target和扩展Target目录中都生成了一个entitlements类型文件,记录了一个App Groups项。
这个共享的容器,就是存放扩展和宿主App共用的数据的空间。
为了正常编译,还需要前往开发者中心,编辑主应用和扩展的AppID,开启支持App Groups功能,类似于开启推送功能。
配置完成后,就是使用了。不管是采用UserDefaults、Archive、CoreData、FMDB、LevelDB等哪种数据存储或操作方式,只需要将路径指向共享的容器路径就可以。
例如使用UserDefaults方式:
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.xxx"]; [defaults setObject:@"xxxxx" forKey:@"phoneNum"];
只是打开了指定的UserDefaults对象,使用的实例方法为initWithSuiteName。
其他方式同理。
6.类库和资源共享
前面提到,扩展需要使用宿主Target的类库和资源,可以直接拷贝到扩展Target目录下,但是这样做会增加应用体积。
另一个方法,就是创建framework动态链接库。新建File->New->Target->Framework,得到一个新的Target,与系统的框架类似,我们可以自由添加类文件封装功能,只暴露public头文件供调用。在Build Phases栏的Headers项中,将Project组的头文件拖动到Public组即可。
framework也有Bundle ID,但是不需要关注,并且不需要配置证书。
需要注意的是,新建的动态链接库,一般会自动创建同名的头文件,可以直接在其中引用需要暴露的自定义类头文件。但是如果删除了该头文件,可能会有如下警告:
warning: no umbrella header found for target 'OrderFoodTodayKit', module map will not be generated
另外一个需要特别注意的地方,因为我们创建的动态链接库是需要提供给扩展使用的,而扩展不支持部分Api,所以该framework中就需要除去扩展不支持的部分Api。在framework的General栏的Deployment Info中,勾选Allow app extension API only。这样的话,如果在framework中使用了扩展不支持的Api,编译会报错。
7.Today Extension调起宿主App
如果需要从Today Extension中调起宿主App,可以使用宿主App的Url Scheme方式:
[self.extensionContext openURL:[NSURL URLWithString:@"xxxx://xxxx"] completionHandler:nil];
以上就是开发过程中,需要注意的事项记录。
完整的开发教程,推荐查阅:http://www.cocoachina.com/ios/20141023/10027.html