iOS8.0加入了扩展,iOS10苹果又增加了很多扩展。在今后,程序中会集成越来越多的扩展功能。
今天主要来模仿1password实现在其他APP登录时自动填充账号、密码。通过这一功能开发了解扩展。
这是一个很有意思的功能。
我们先来看功能实现效果。
1passwrod是一款密码管理类app,我们可以在登录时唤醒1password并获取到相应的账号密码,然后填充到输入框中以实现账号密码的自动填充、登录。
这种app之间的互相访问、数据共享看起来与我们以往的开发经历所不同。这种功能实现,不仅使APP更加灵活,还提升用户体验。
如何实现的呢,看完这篇文章,你也能学会。
先来创建一个demo工程,工程名为ExtensionDemo。
网上的文档有很多,但基本都以一个简单的demo为主,在我创建的demo中,涉及到了宿主应用和应用扩展的数据库共享、类共享、xib共享、以及宿主应用和应用扩展、应用扩展和host app的相互通信。把需求实现过程中遇到的坑全部描述清晰,帮助小伙伴少走弯路。
开始之前,我们需要了解一些理论知识。
host app:通过点击系统分享菜单中的插件图标调起扩展程序,在gif图片中,唤起1password的应用就是host app。
宿主应用:也叫Containing App,简单点说,我们创建一个Xcode工程,然后运行项目,这个就是宿主应用。
应用扩展:也叫App Extension,打包运行在手机上时,会随着宿主应用一起安装在手机上。详细来说,gif图中唤起的应用并不是1password本身,而是1password的应用扩展。是独立于宿主应用之外的。
总结一下:应用扩展就是宿主应用和host app沟通的桥梁,使宿主应用和Host App的数据共享成为可能。
他们的关系图如下:
宿主应用 & 应用扩展
好,开始。
宿主应用和应用应用是在一个工程下,用户安装APP后,如果工程内有应用扩展,应用扩展也会默认安装在用户的手机上。
先来看一下宿主应用的显示效果。
工程文件
这里为了方便,使用PasswordDBTool来操作数据库,没有使用Key-Value式的存储,不过这里不是本次的重点。数据库相关下次来写。
好,到这里,宿主应用所需要的东西我们都搞定了。接下来,开始应用扩展的开发和相应的配置。
在之前,我们已经了解到,应用扩展属于应用的扩展。扩展是iOS8.0加入的一个非常强大的功能。接下来开始在项目中加入扩展。
1、添加扩展Target
2、操作完的工程文件
这些都是添加完扩展target后系统默认为工程生成的。
当然,ActionViewController的.h.m文件和MainInterface.storyboard文件我们都可以随便的对其更改。其实这三个文件和我们平时创建使用的类文件和storyboard文件并无两样。同样支持拖线等操作。
3、接下来我们看一下ActionViewController的.h和.m文件中的代码内容。
系统创建的ActionViewController默认继承自UIViewController,当然我们也可以对这里进行更改,让其继承自UITableViewController以便之后的开发。
重点来讲一下图2中的代码内容。
(1)self.extensionContext
command+鼠标左键点进去看看,发现是这样的。
发现self.extensionContext是NSExtensionContext对象。见名知意,extensionContext即扩展上下文,用来联系宿主应用和应用扩展,它们俩之间的通信就是靠extensionContext。
(2)NSExtensionItem
待处理的数据,宿主应用和应用扩展之间通信的数据(参数等)我们可以放到NSExtensionItem对象中。在各自的应用中通过NSExtensionItem获取通信数据。
(3)NSItemProvider
确切来说,宿主应用和应用扩展之间需要传递的数据是放在NSItemProvider对象中的。
那么,NSItemProvider对象是如何进行数据存储的?重点在这里。
通过NSItemProvider对象的
loadItemForTypeIdentifier:options:completionHandler:方法。
这里有一个特别需要注意的点,就是第一个参数的传值。command+鼠标左键点击第一个参数KUTypeImage,进去会发现有几十个这样的参数。当然,每一种参数的含义都不相同,这里不一一详解。如果这里的参数值传的是KUTypeImage则相应的,宿主应用传递过来的数据是一个图片。如果这里的参数值传的是kUTTypePropertyList,相应的,宿主应用传递过来的数据可能是一个字典。
但是在我们的demo中,我们不使用系统提供的这些参数,而使用自定义参数。格式如下:
具体是什么含义会在下面陆续讲解。因为这里需要host app协同操作才能看的更明白。
应用扩展
我们都知道,iOS应用具有沙盒机制。app之间是不能进行数据共享的。而在文章开头展示的gif图却给我们造成一种假象,即我们在app中可以去访问其他app的数据,有种“app之间可以进行数据共享”的错觉。而这种错觉就是应用“扩展”给我们造成的,扩展使app之间的数据共享成为了一种可能。使app变得更加灵活。
现在,我们要实现的需求是这样的:在host app中唤起应用的扩展,host app需要传给应用扩展一个URL参数,应用扩展根据host app传递过来的URL参数在宿主应用内的数据库中查找符合条件的数据,再把符合条件的数据回传给host app。
整个流程是这样的。
在整个通信过程中,难点在于宿主应用和应用扩展的数据共享,不仅仅是数据共享,可能还需要共享一些开发文件,比如类文件、xib、storyboard等。不要以为宿主应用和应用扩展同属于一个工程项目,它们两个就可以共同使用项目内的数据和所有文件。这是错误的。那么,宿主应用和应用扩展如何进行数据共享?我们需要创建一个共享域,当然,苹果早就给我们准备好了,我们只需要配置一下即可。
1、配置共享域
(1)配置宿主应用共享域
点击ON后,其实App Groups这里是空的,因为我之前做项目有配置过共享域,所以在选择证书的时候,系统会把证书配置过的共享域都给我自动加载了出来。如果这里是空的,就点击下面的+号,添加一个共享域。
这时,Xcode会弹出提示框,让你给共享库起一个名字以辨别,因为有些项目可能需要不只一个共享域,如果项目支持Apple watch,就需要一个新的共享域支持Apple watch。共享域的名字以group.开头,名字自己起。
OK,添加完共享域后,新的共享域就出现在了APP Groups中,选中它。
到这里,宿主应用的共享域配置告一段落。
(2)配置应用扩展
点击ON后,系统会弹出提示框,让你选择证书,因为共享域是在证书的基础上配置的。证书选择后,会把对应的所有共享域显示在App Groups中。
选中我们之前在宿主应用创建(选择)的共享域。
OK,应用扩展的共享域配置完毕。
2、数据共享
(1)NSUserDefaults
NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.testAppExtension"];
获取共享域的偏好设置
接下来平时怎么用这里就怎么用。
(2)数据库
在创建应用扩展前,数据库我是放到这个路径下的。
[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"TestDB.sqlite"]
而现在,即使共享域配置完毕,应用扩展继续访问这个路径下的数据库也是访问不到的,因为共享域它有自己的路径。宿主应用和应用扩展之间的空间关系如下:
所以,我们要将数据库放在共享域的路径下。共享域的路径如下:
[[[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.testAppExtension"] absoluteString] stringByAppendingPathComponent:@"TestDB.sqlite"]
通过containerURLForSecurityApplicationGroupIdentifier方法和共享域标识符我们可以获取到该共享域的路径
OK,共享数据到这里暂告一段落。
3、应用扩展开始编码
前面说了这里可以随便改,修改后的结构如下:
这里别忘了把新的storyboard和控制器关联一下。
然后我们来看ActionViewController.m文件。
target选择PassowrdAppExtension进行调试。
然后选择在哪个host app中进行测试。
点击RUN,报错。
通过错误信息可以知道是文件引用错误。
这是因为此时应用扩展还不能随便使用项目内的其他文件。因为到目前为止,都是宿主应用的target在引用这些文件。
看到这个错误我的第一反应是把Password和PasswordDBTool的类文件加入到应用扩展 target 的编译文件中去,这样在扩展中自然也就可以使用了。但是,文件数量少,这样做还可以。如果文件数量大,再这样做会十分麻烦,出错的概率会大大增加,效率也十分低下,所有类弄的团团糟维护起来也很麻烦。所幸我们可以创建一个Framework文件,让Framework文件引用这些需要共享的类,再让宿主应用和应用扩展分别导入Framework文件。这样做就很好的解决了问题,还不容易出错,也便于后期维护。
一步一步来实现刚才说的。
1、创建framework文件
framework文件的命名规范一些,以Kit为结尾。
创建完framework后工程目录如下
2、引用文件
(1)先把宿主应用target的文件引用删除,因为应用扩展同样要使用FMDB,所以也要把第三方文件从target中删除,否则编译照样会报错。
点击Compile Sources下面的-号把标注的类全部删除。
最后只剩下3个文件。
(2)增加AppExtensionKit的引用文件
点+号把刚才删除的类加进来。添加完后如下:
需要注意的是,在这里不要添加xib文件,xib在哪修改下面会说。
(3)为应用扩展导入AppExtensionKit文件
添加完后编译一下,报错,40多个。
这是因为应用扩展也要用到libsqlite3.0.tbd这个包,但是并没有为应用扩展添加这个包,所以,重复上面的操作,把libsqlite3.0.tbd加入到AppExtensionKit中。
再编译一下,错误全部消失不见。OK,配置全部完成。
(4)丰富一下ActionViewController.m的代码,把共享区数据库的数据全部打印出来。
编译无错,运行崩溃。崩溃位置是第40行。
原因:PasswrodCell是从xib加载的,但我们并没有把xib文件加入到AppExtensionKit中。知道问题出在哪了,去解决。
在宿主应用的target中,找到PasswordCell的引用并删除。如下:
在targets中选中AppExtesnionKit,为其添加Password.xib的引用,如下:
操作完后,xib文件从原来bundle下的路径变成了bundle下AppExtensionKit下的路径。
做完这些还不够,我们还要在ExtensionDemo和PasswordAppExtension两个target下的Copy Bundle Resources中将AppExtensionKit导入进来,否则宿主应用和应用扩展还是用不了PasswordCell.xib。如图:
ExtensionDemo的target:
PasswordAppExtension的target:
那我们再次加载Password.xib文件,就需要从Bundle下的AppExtensionKit文件中加载。
加载方式代码如下:
cell = [[NSBundle mainBundle] loadNibNamed:@"AppExtensionKit.framework/ExtensionCell" owner:nil options:nil].lastObject;
运行项目,效果如图:
和宿主应用显示的数据一模一样。
自此,宿主应用与应用扩展的数据共享就完成了。
接下来,是Host App和应用扩展之间的数据传递。
Host App
Host App界面实现和代码逻辑都比较简单。
实现效果如下:
代码部分:
点击按钮时会触发如下代码:
这里有几个关键点:
(1)首先,我创建了一个字典并且保存了两个参数,一个是版本号,一个是URLKey(我要将这个参数传递给应用扩展,应用扩展会用这个key做为查询条件到数据库中查询数据,然后将查询到的数据再回传给host app)。
(2)我把这个字典赋值给了NSItemProvider的item属性,又将NSItemProvider对象添加到了NSExtensionItem对象的attachments数组中。在应用扩展中,我们也按照这种方式来逐步获取字典。
(3)前面说过,系统提供了KUTTypeImage等字段用来在应用扩展中获取来自host app传递过来的值,而这个字段我们是可以自定义的。如图,这个自定义字段也是通过NSItemProvider对象来传递的。
(3)在应用扩展中,我们如何通过这个自定义字段来获取host app传递过来的数据。如图:
关键代码已经用红色方框标注出来了。
也就是说通过这句代码我们可以获取到host app向应用传递的typeIdentifier。这两个地方要一致才能获取到host app传递过来的数据。
在block回调中把host app传递过来的数据取出来,然后到数据库中进行查询就可以了。
(4)数据查询到了怎么回传给host app呢?
刚才已经展示过了应用扩展的界面,应用扩展实现了与宿主应用的数据共享。如图:
当点击右上角关闭按钮时,什么数据都不回传。
当点击某个cell时,把对应的数据(也就是某条密码)回传给Host App,并把该密码的账户和密码显示在对应的输入框中。
代码如下:
关闭按钮的点击事件:
单元格点击事件:
到这里,应用扩展对host app的数据回传就搞定了。
(5)host app拿到回传数据进行登录
这一步是通过UIActivityViewController对象的回调完成的。
不管是把数据从host app传给应用扩展,
还是把数据从应用扩展传给host app,
数据的传递依靠的都是NSExtensionItem和NSItemProvider,
如果非要给他们弄一个关系便于理解的话,大概是这样的:
存:
需要传递的数据 -> NSItemProvider -> NSExtensionItem -> NSExtensionContext
取:
NSExtensionContext -> NSExtensionItem -> NSItemProvider -> 拿到需要传递的数据
一层一层的包裹着。
OK,全部搞定。
我们来看一下最终的效果。
额,还差一点。
没有给我们的应用扩展配置一个图标。
OK,全部搞定。
需求实现了。
但是在使用扩展的过程中还是有不少的坑,为了谨慎起见,在扩展中编写代码调用方法,多看看文档。有很多方法都有官方注释,有些方法是不能在应用扩展中使用的。
好,今天就到这里。