iOS Extentsion 入门实战
最近需求是App接入 Today Extentsion
组件,因为之前没做过这方面,都是摸着石头过河.以下文章都是基于摸索中遇到问题和一些总结,如果有什么不对,请指教一下~
1. App Extensions简介
1.1 什么是App Extensions
从 iOS 8 开始,苹果引入了全新的 App Extension。它是一种扩展,很类似于一些大型软件的插件机制。App Extension 事实上并不是你应用的插件,而是系统的插件,其生命周期是由系统来管理的,所以如果你想做什么坏事还是行不通的…但是 App Extension 分发的载体是应用,也就是说如果你只是单纯想做一个今日面板插件,也需要有个主程序,你的主程序可以什么都不做,也可以提供一些基本的设置和数据。iOS的Extension包括以下:
文章用到的是 Today Extension
,用于 iPhone 的今日插件
1.2 App Extension 和主程序的关系
可以说没有什么关系,基本上就是两个独立的程序,你的主程序既不可以访问 App Extension 的代码,也不可以访问其存储空间,这完完全全就是两个进程、两个程序。
1.3 App Extension 可以干什么?不可以干什么?
基本上什么都能干,但是内存有限制,App Extension 的可用内存远不如常规应用。也不能执行长时间的操作,你的 App Extension
可能随时被系统 Kill 掉,因为 App Extension
是由系统管理生命周期的。最后一些API也是不能用,一部分如下:
- 不能使用 sharedApplication 对象及其其中的方法。
- 使用NS_EXTENSION_UNAVAILABLE 宏在头文件中标记的任意API或类似的不可用的宏或API在一个不可用的框架中。例如,在iOS 8.0中,the HealthKit framework and EventKit UI framework 将不能使用app extension.
- 不能在iOS设备中使用相机和麦克风(不像其他app extensions,iMessage app可以访问这些资源,只有它正确的配置 NSCameraUsageDescription and NSMicrophoneUsageDescription Info.plist 文件中的Key)。
- 不能长期运行后台任务,该限制的具体细节因平台而异。(一个app extension能够使用NSURLSession对象启动一个上传或下载并将这些操作的结果返回给containing app)
- 不能使用Air Drop接收数据(一个app extension能够像一个正常的app一样通过UIActivityViewController class去使用AirDrop发送数据)
还有更多不可用的 API 可以看这个苹果官方文档(以文档为主):Understand How an App Extension Works
1.4 App Extension 生命周期与交互
因为app extension不是一个app,它的生命周期和app是不同的,在大多情况下,当用户从app的UI上或者其他活动视图控制器中选择开启extension功能的选项时将会开始执行。
先说下几个概念:
- App : 就是我们正常手机里的每个应用程序,即Xcode运行后生成的程序。一个app可以包含一个或多个target,每个target将产生一个product.
- App extension : 为了扩展特定app的功能并且依赖于一个特定的app的一条进程。
- Containing app:一个app包含一个或多个extension称为containing app。
- target : 在项目中新建一个target来创建app extension.任意一个target指定了应用程序中构建product的设置信息和文件。
- host app : 包含app extension 并且能从中打开它(并不一定非要从此app内部打开,可以是app内部也可以是例如Today Widget外部控件)。
host app
: 我们可以把它理解为宿主的App
,能够调起extension的app被称为host app,比如:Safari app 里面网页分享到微信, Safari就是 host app ; widget的host app就是Today。
App Extension基本认知:
- Extension不能单独发布和部署,需要依赖于容器应用(Containing App)。
- Extension和容器应用(Containing App)的生命周期是独立的,分别为两个不同的进程.
- Extension的运行依赖于宿主应用(Host App),生命周期由宿主应用决定。
- Extension作为一个单独的target存在,但会随着容器应用(Containing App)的安装和卸载而安装和卸载。
- Extension需要独立的证书用来打包和测试。
1.4.1 Extension的生命周期如下:
调用起了 Extension
,操作完就退出
1.4.2 Extension 与 App 交互
一些简单的交互
- app extension 和 containing app将不能够直接交互,典型的,containing app可能还没有开始运行然而它包含的app extension已经在运行了。(例如,一个天气的app,当你还没有打开它时,你可以在Today Widget中看到今天天气的信息)
- 在一个典型的请求响应事务中,系统代表的host app打开app extension, 通过host提供的extension上下文(context)传递数据, 这个extension通过界面的展示来执行一些任务,如果适用于extension的目的,返回数据给host.
- 上图的虚线代表了app extension 和 containing app之间有限的交互。例如Today widget(只有 Today Extension 才支持通过调用其他不可以) 通过调用 NSExtensionContext类中
openURL:completionHandler:
方法来要求系统去打开它的containing app.
具体交互
任意一个app extension和它的containing app能够在一个私有的shared container中分享数据。
2. 实战
要做的事情:
使用 Extensions
难免会与Containing App
有一定交互或者资源共享,首先列出了可能会遇到的情景:
widget 和 主 App 共用资源
widget 和主 App 共享代码和资源。我们还是要尽可能的让 widget 和主 App 共享代码。因为没必要一份代码写2次,能复用就复用
主要有两个方案:
- framework
- 直接共享
widget 和 主 App 共享数据
严格来说 widget 和 App 是不同的两个 App 了, 而且他们之间有沙盒限制,他们之间要共享数据的话只能使用 App Groups 了。常用就是 NSUserDefault
、Core Data
、SQL
2.1 创建项目的 Extension
1 | XCode -> File -> New -> Target |
选择 Today Extension
, 项目名字随便取,当然最好就是你的跟你项目相关了,创建后可以看到,几个地方多了这个Extension
显示了:
项目中多了个文件夹,文件夹名字就是 Extension
命名的名字了
target中多了 Extension
纵观一下文件, TodayViewController
是 Extension
主体文件了,业务逻辑也写在里面,默认新创建的Extension
是使用 .storyboard
管理UI视图的,当然你不想用也行,只需要在 Extension
里的 info.plist
改下
改 NSExtensionMainStoryboard
字段为 NSExtensionPrincipalClass
,并修改入口为自定义类。
然后选择 scheme可以编译看下 helloworld
效果了
2.2 Pod 的配置
Extension
开发中有时难免会用到一些第三方,如 SDWebImage
或者AFNetworking
之类的,Cocoapod
也提供了这方面功能,能使 Extension
用上这些类,同理在 Podfile
中添加
1 | target 'TodayExtentsionDemo' do |
此时你也许会留意到,比如上面 AF
两个Target都要用到,都要重新写一次,会很麻烦。
是很麻烦,故此上网找了下相关解决方法,知道有个这个 link_with
方法,如下:
1 | link_with 'TodayExtentsionDemo', 'TodayExtentsion' |
但是,这种方法已经弃用了, Pod install
会报错,所以你看到网上介绍相关方法的文章,都是抄别人自己又不试的
由于 Cocoapod
是 Ruby
写的,我们可以这样子操作一下
1 | def shareFramework |
Pod install
之后即可为每个 Target
引入相关库
课外阅读: Cocoapods原理总结
此时,我们可以在 Containning App
与 Extension
中使用 AF
库了
2.3 Extension 入门 UI 操作
我们刚编译的 Extension
你会看到是没有展开按钮的,此时我们只需要在 viewDidLoad
中添加
1 | //这个iOS10以后的API,可以在Extension上显示 展开/收起 按钮, 至于iOS10 之前需要搞一个按钮手动触发改变高度? |
添加了这句代码 之后,你会发现 Extension
上有了折叠收起按钮了
1 | - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize { |
这个是 Extension
代理方法,用于回调点击了 收起/展开 按钮后,返回 显示区域的高度大小
经过一些测试,发现这个 Extension
最大的高度只能是 屏幕高度 - 139. 超过了也是这么高
else 下的是收起高度,直接返回代理给你的系统 高度好了(没有特别要求的话)
1 | - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler{ |
大致意思是建议你通过实现它的回调来获取数据,你也可以在viewwillappear中设置你的初始数据,但是希望你在获取到新数据时能平稳的过渡到新的数据来。并且它是在主线程更新。
当widget最初被创建时,widgetPerformUpdateWithCompletionHandler几乎被简单地调用,所以你可以在这里完成所有的加载,但是苹果建议你尽可能的在生命周期的早期开始加载过程。
如果您的小部件显示的信息永远不会更改,那么您不必在widgetPerformUpdateWithCompletionHandler
中执行任何操作。
其实可以理解为 类似天气或者股票之类App,在用户使用时候,系统每隔一段时间会调用这个方法获取 Extension
的快照,比如一段时间获得当前天气情况,更新Extension
,下次你看到这个 Extension
时候就是最新的天气了
2.4 Extension 与 Containing App 代码共享
Extension
也有UI,也有业务逻辑,有时候跟项目一些流程相同,那么代码复用也是一个考虑的点。
由于 Extension
与 Containing App
不算同一个App且同一个进程,如果要代码共享,很多人会考虑直接拷贝一份代码放到 Extension
里面(即Target
选多一个Extension
),比如,我们 Extension
里面需要用到一个 Tableview
显示数据,Containing App
中也有相应的 Tableview
用到这个 Cell
,
比如你在 Extension
里面引入这个类并且写逻辑,你会发现并不能通过编译,因为Extension
无法访问这个,此时最简单方法就是
在 Target Membership
中勾选 Containning App
与 Extension
即可
但是一个问题来了,比如你这个 Cell 还引用了其他类,其他类的 Target Membership
也是需要勾选的,这样一来,项目一大你不知道有哪些勾了和没勾。这只是其中一个小问题,
另外的问题就是.如果这个文件还是主程序的 target
,只要改动一下,所属的target就被编译器标记为需要重新编译,这样整个App就需要重新编译(当然你Run也是重新编译一次),但是如果你用下面所说的 framework
苹果推荐方法,只需要编译framework
里面的文件,达到组件化。我们知道cocoapod
也是将第三方库编译成 framework
避免每次编译App也编译这部分东西的.
具体原理可以参考下面 文章 用 Framework 重構 Swift 程式碼 大大提高編譯效率!
做法如下:
首先在新建一个 Framework
:
1 | XCode -> File -> New -> Target |
通常 Framework
都以 xxxkit
结尾作为标准规范,新建完成后会发现架构中多了这个Framework
文件夹以及 Target
中多了一个(情况如上面新建Extension
一样,不做更多截图)
第二 , 在Containning App
的 Target
中删除共用代码的文件
然后再 Framework
的 Complie Sources
添加这个共用类
最后,在 Extension
中添加 Link Binary With Libraries
这样既可完成 Extension
与 Containing App
共用类的编译以及运行
如何解决警告linking against dylib not safe for use in application extensions:
因为app extension限制了某些API的使用, ( App Extensions不能使用的一些API ) ,因此在自定义自己的framework后,这个framework可能包含了某些在App Extensions里不能使用的API,因此为了安全起见才会给出这个警告。
选中自定义framework的target,然后选中Build Settings,(记住选择All,而不是Basic),在过滤框中输入”require only”,将Require Only App-Extension-Safe API
的值改成YES,(默认为NO),然后Command + K clean一下工程,警告久消除了。
2.5 Widget 调用 Containing App
这个跟 urlScheme 设置一样,主要就是因为 Extension
不能调用 [UIApplication sharedApplication]
所以正确做法是
1 | self.extensionContext openURL:<#(nonnull NSURL *)#> completionHandler:<#^(BOOL success)completionHandler#> |
这里不做更多叙述,可以通过URL传递参数,APP动态路由处理即可
2.6 Widget 与 Containing App 数据共通
严格来说 widget 和 App 是不同的两个 App 了, 他们之间要共享数据的话只能使用 App Groups 了。
具体做法忽略,暂时用个人开发者账户,之前创建的 AppGroup 发现不能删除..等上了公司证书再补坑
最后附上 Demo 地址 : Demo
Reference
1.揭秘 iOS App Extension 开发 —— Today 篇
3.App Extensions Increase Your Impact
iOS Extentsion 入门实战