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 了。常用就是 NSUserDefaultCore DataSQL

2.1 创建项目的 Extension

1
XCode -> File -> New -> Target

选择 Today Extension , 项目名字随便取,当然最好就是你的跟你项目相关了,创建后可以看到,几个地方多了这个Extension 显示了:

项目中多了个文件夹,文件夹名字就是 Extension 命名的名字了

target中多了 Extension

纵观一下文件, TodayViewControllerExtension主体文件了,业务逻辑也写在里面,默认新创建的Extension是使用 .storyboard 管理UI视图的,当然你不想用也行,只需要在 Extension里的 info.plist改下

NSExtensionMainStoryboard字段为 NSExtensionPrincipalClass ,并修改入口为自定义类。

然后选择 scheme可以编译看下 helloworld 效果了

2.2 Pod 的配置

Extension开发中有时难免会用到一些第三方,如 SDWebImage 或者AFNetworking之类的,Cocoapod 也提供了这方面功能,能使 Extension 用上这些类,同理在 Podfile 中添加

1
2
3
4
5
6
7
target 'TodayExtentsionDemo' do
pod 'AFNetworking'
end
target 'TodayExtentsion' do
pod 'AFNetworking'
end
# target写 App 与 Extension 名字即可关联

此时你也许会留意到,比如上面 AF 两个Target都要用到,都要重新写一次,会很麻烦。

是很麻烦,故此上网找了下相关解决方法,知道有个这个 link_with 方法,如下:

1
2
link_with 'TodayExtentsionDemo', 'TodayExtentsion'
pod 'AFNetworking'

但是,这种方法已经弃用了, Pod install会报错,所以你看到网上介绍相关方法的文章,都是抄别人自己又不试的

由于 CocoapodRuby 写的,我们可以这样子操作一下

1
2
3
4
5
6
7
8
9
10
11
12
13
def shareFramework
pod 'AFNetworking'
#blablabla Framework
end

target 'TodayExtentsionDemo' do
shareFramework
end
target 'TodayExtentsion' do
shareFramework
end

#定义了个Function,在里面添加通用的库

Pod install之后即可为每个 Target 引入相关库

课外阅读: Cocoapods原理总结

此时,我们可以在 Containning AppExtension中使用 AF库了

2.3 Extension 入门 UI 操作

我们刚编译的 Extension 你会看到是没有展开按钮的,此时我们只需要在 viewDidLoad 中添加

1
2
3
4
5
6
7
//这个iOS10以后的API,可以在Extension上显示 展开/收起 按钮, 至于iOS10 之前需要搞一个按钮手动触发改变高度?
//由于手上没有 iOS10 之前机子测试, 所以先挖个坑,迟点有了再补坑
if (@available(iOS 10.0, *)) {
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}else{

}

添加了这句代码 之后,你会发现 Extension上有了折叠收起按钮了

1
2
3
4
5
6
7
8
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
if (activeDisplayMode == NCWidgetDisplayModeExpanded) {
// 设置展开的新高度
//最大只能 screen size - 139
}else{
self.preferredContentSize = maxSize;
}
}

这个是 Extension代理方法,用于回调点击了 收起/展开 按钮后,返回 显示区域的高度大小

经过一些测试,发现这个 Extension最大的高度只能是 屏幕高度 - 139. 超过了也是这么高

else 下的是收起高度,直接返回代理给你的系统 高度好了(没有特别要求的话)

1
2
3
4
5
6
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler{
//blablabla
//做vieWillAppear前数据获取之类的操作
//或者系统每隔一段时间截取 Extension 图片时候调用的方法
completionHandler(NCUpdateResultNewData);
}

大致意思是建议你通过实现它的回调来获取数据,你也可以在viewwillappear中设置你的初始数据,但是希望你在获取到新数据时能平稳的过渡到新的数据来。并且它是在主线程更新。

当widget最初被创建时,widgetPerformUpdateWithCompletionHandler几乎被简单地调用,所以你可以在这里完成所有的加载,但是苹果建议你尽可能的在生命周期的早期开始加载过程。

如果您的小部件显示的信息永远不会更改,那么您不必在widgetPerformUpdateWithCompletionHandler中执行任何操作。

其实可以理解为 类似天气或者股票之类App,在用户使用时候,系统每隔一段时间会调用这个方法获取 Extension的快照,比如一段时间获得当前天气情况,更新Extension,下次你看到这个 Extension时候就是最新的天气了

2.4 Extension 与 Containing App 代码共享

Extension也有UI,也有业务逻辑,有时候跟项目一些流程相同,那么代码复用也是一个考虑的点。

由于 ExtensionContaining App 不算同一个App且同一个进程,如果要代码共享,很多人会考虑直接拷贝一份代码放到 Extension 里面(即Target选多一个Extension),比如,我们 Extension 里面需要用到一个 Tableview 显示数据,Containing App中也有相应的 Tableview 用到这个 Cell,

比如你在 Extension 里面引入这个类并且写逻辑,你会发现并不能通过编译,因为Extension 无法访问这个,此时最简单方法就是

Target Membership中勾选 Containning AppExtension 即可

但是一个问题来了,比如你这个 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 AppTarget中删除共用代码的文件

然后再 FrameworkComplie Sources 添加这个共用类

最后,在 Extension 中添加 Link Binary With Libraries

这样既可完成 ExtensionContaining 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 篇

2.App extension 总结

3.App Extensions Increase Your Impact

4.Understand How an App Extension Works

5.Cocoapods原理总结

6.用 Framework 重構 Swift 程式碼 大大提高編譯效率!

iOS Extentsion 入门实战

https://swlfigo.github.io/posts/6244/

Author

Sylar

Posted on

2019-04-28

Updated on

2021-11-14

Licensed under

Comments