傲视苍穹iOS《Objective-C》VIP专题OC开发开发

iOS Action Extension开发教程,实现跨APP的

2016-08-08  本文已影响3305人  fou7

iOS8.0加入了扩展,iOS10苹果又增加了很多扩展。在今后,程序中会集成越来越多的扩展功能。

今天主要来模仿1password实现在其他APP登录时自动填充账号、密码。通过这一功能开发了解扩展。

这是一个很有意思的功能。

我们先来看功能实现效果。

fdfasiiyiuyuiyi.gif

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的数据共享成为可能。

他们的关系图如下:

图1.jpg 图2.jpg

宿主应用 & 应用扩展

好,开始。

宿主应用和应用应用是在一个工程下,用户安装APP后,如果工程内有应用扩展,应用扩展也会默认安装在用户的手机上。

先来看一下宿主应用的显示效果。

iPhone6 Plus

工程文件

工程文件

这里为了方便,使用PasswordDBTool来操作数据库,没有使用Key-Value式的存储,不过这里不是本次的重点。数据库相关下次来写。

好,到这里,宿主应用所需要的东西我们都搞定了。接下来,开始应用扩展的开发和相应的配置。

在之前,我们已经了解到,应用扩展属于应用的扩展。扩展是iOS8.0加入的一个非常强大的功能。接下来开始在项目中加入扩展。

1、添加扩展Target

Snip20160808_148.png Snip20160808_149.png Snip20160808_150.png Snip20160808_151.png

2、操作完的工程文件

Snip20160808_152.png Snip20160808_153.png

这些都是添加完扩展target后系统默认为工程生成的。

当然,ActionViewController的.h.m文件和MainInterface.storyboard文件我们都可以随便的对其更改。其实这三个文件和我们平时创建使用的类文件和storyboard文件并无两样。同样支持拖线等操作。

3、接下来我们看一下ActionViewController的.h和.m文件中的代码内容。

ActionViewController.h ActionViewController.m

系统创建的ActionViewController默认继承自UIViewController,当然我们也可以对这里进行更改,让其继承自UITableViewController以便之后的开发。

重点来讲一下图2中的代码内容。

(1)self.extensionContext
command+鼠标左键点进去看看,发现是这样的。

Snip20160808_156.png

发现self.extensionContext是NSExtensionContext对象。见名知意,extensionContext即扩展上下文,用来联系宿主应用和应用扩展,它们俩之间的通信就是靠extensionContext。

(2)NSExtensionItem
待处理的数据,宿主应用和应用扩展之间通信的数据(参数等)我们可以放到NSExtensionItem对象中。在各自的应用中通过NSExtensionItem获取通信数据。

(3)NSItemProvider
确切来说,宿主应用和应用扩展之间需要传递的数据是放在NSItemProvider对象中的。
那么,NSItemProvider对象是如何进行数据存储的?重点在这里。

Paste_Image.png

通过NSItemProvider对象的
loadItemForTypeIdentifier:options:completionHandler:方法。
这里有一个特别需要注意的点,就是第一个参数的传值。command+鼠标左键点击第一个参数KUTypeImage,进去会发现有几十个这样的参数。当然,每一种参数的含义都不相同,这里不一一详解。如果这里的参数值传的是KUTypeImage则相应的,宿主应用传递过来的数据是一个图片。如果这里的参数值传的是kUTTypePropertyList,相应的,宿主应用传递过来的数据可能是一个字典。
但是在我们的demo中,我们不使用系统提供的这些参数,而使用自定义参数。格式如下:

Snip20160808_158.png

具体是什么含义会在下面陆续讲解。因为这里需要host app协同操作才能看的更明白。

应用扩展

我们都知道,iOS应用具有沙盒机制。app之间是不能进行数据共享的。而在文章开头展示的gif图却给我们造成一种假象,即我们在app中可以去访问其他app的数据,有种“app之间可以进行数据共享”的错觉。而这种错觉就是应用“扩展”给我们造成的,扩展使app之间的数据共享成为了一种可能。使app变得更加灵活。

现在,我们要实现的需求是这样的:在host app中唤起应用的扩展,host app需要传给应用扩展一个URL参数,应用扩展根据host app传递过来的URL参数在宿主应用内的数据库中查找符合条件的数据,再把符合条件的数据回传给host app。

整个流程是这样的。

20150114200552609.jpg

在整个通信过程中,难点在于宿主应用和应用扩展的数据共享,不仅仅是数据共享,可能还需要共享一些开发文件,比如类文件、xib、storyboard等。不要以为宿主应用和应用扩展同属于一个工程项目,它们两个就可以共同使用项目内的数据和所有文件。这是错误的。那么,宿主应用和应用扩展如何进行数据共享?我们需要创建一个共享域,当然,苹果早就给我们准备好了,我们只需要配置一下即可。

1、配置共享域
(1)配置宿主应用共享域

Snip20160808_160.png Snip20160808_163.png

点击ON后,其实App Groups这里是空的,因为我之前做项目有配置过共享域,所以在选择证书的时候,系统会把证书配置过的共享域都给我自动加载了出来。如果这里是空的,就点击下面的+号,添加一个共享域。

这时,Xcode会弹出提示框,让你给共享库起一个名字以辨别,因为有些项目可能需要不只一个共享域,如果项目支持Apple watch,就需要一个新的共享域支持Apple watch。共享域的名字以group.开头,名字自己起。

Snip20160808_162.png

OK,添加完共享域后,新的共享域就出现在了APP Groups中,选中它。

到这里,宿主应用的共享域配置告一段落。

(2)配置应用扩展

Snip20160808_165.png

点击ON后,系统会弹出提示框,让你选择证书,因为共享域是在证书的基础上配置的。证书选择后,会把对应的所有共享域显示在App Groups中。

Snip20160808_168.png Snip20160808_170.png

选中我们之前在宿主应用创建(选择)的共享域。

OK,应用扩展的共享域配置完毕。

2、数据共享
(1)NSUserDefaults

NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.testAppExtension"];
获取共享域的偏好设置

接下来平时怎么用这里就怎么用。
(2)数据库
在创建应用扩展前,数据库我是放到这个路径下的。

[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"TestDB.sqlite"]

而现在,即使共享域配置完毕,应用扩展继续访问这个路径下的数据库也是访问不到的,因为共享域它有自己的路径。宿主应用和应用扩展之间的空间关系如下:

Snip20160808_172.png

所以,我们要将数据库放在共享域的路径下。共享域的路径如下:

[[[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.testAppExtension"] absoluteString] stringByAppendingPathComponent:@"TestDB.sqlite"]

通过containerURLForSecurityApplicationGroupIdentifier方法和共享域标识符我们可以获取到该共享域的路径

OK,共享数据到这里暂告一段落。

3、应用扩展开始编码


Snip20160808_173.png

前面说了这里可以随便改,修改后的结构如下:

Snip20160808_174.png

这里别忘了把新的storyboard和控制器关联一下。

然后我们来看ActionViewController.m文件。


Snip20160808_176.png

target选择PassowrdAppExtension进行调试。

然后选择在哪个host app中进行测试。


Snip20160808_178.png

点击RUN,报错。

Paste_Image.png

通过错误信息可以知道是文件引用错误。

这是因为此时应用扩展还不能随便使用项目内的其他文件。因为到目前为止,都是宿主应用的target在引用这些文件。

看到这个错误我的第一反应是把Password和PasswordDBTool的类文件加入到应用扩展 target 的编译文件中去,这样在扩展中自然也就可以使用了。但是,文件数量少,这样做还可以。如果文件数量大,再这样做会十分麻烦,出错的概率会大大增加,效率也十分低下,所有类弄的团团糟维护起来也很麻烦。所幸我们可以创建一个Framework文件,让Framework文件引用这些需要共享的类,再让宿主应用和应用扩展分别导入Framework文件。这样做就很好的解决了问题,还不容易出错,也便于后期维护。

一步一步来实现刚才说的。

1、创建framework文件

Snip20160808_181.png Snip20160808_182.png

framework文件的命名规范一些,以Kit为结尾。

Snip20160808_183.png

创建完framework后工程目录如下

Snip20160808_184.png

2、引用文件
(1)先把宿主应用target的文件引用删除,因为应用扩展同样要使用FMDB,所以也要把第三方文件从target中删除,否则编译照样会报错。

Snip20160808_185.png

点击Compile Sources下面的-号把标注的类全部删除。

Snip20160808_186.png

最后只剩下3个文件。

(2)增加AppExtensionKit的引用文件

Snip20160808_187.png

点+号把刚才删除的类加进来。添加完后如下:

Snip20160808_188.png

需要注意的是,在这里不要添加xib文件,xib在哪修改下面会说。

(3)为应用扩展导入AppExtensionKit文件

Snip20160808_189.png Snip20160808_190.png

添加完后编译一下,报错,40多个。
这是因为应用扩展也要用到libsqlite3.0.tbd这个包,但是并没有为应用扩展添加这个包,所以,重复上面的操作,把libsqlite3.0.tbd加入到AppExtensionKit中。

Snip20160808_191.png Snip20160808_192.png

再编译一下,错误全部消失不见。OK,配置全部完成。

(4)丰富一下ActionViewController.m的代码,把共享区数据库的数据全部打印出来。

Paste_Image.png

编译无错,运行崩溃。崩溃位置是第40行。
原因:PasswrodCell是从xib加载的,但我们并没有把xib文件加入到AppExtensionKit中。知道问题出在哪了,去解决。

在宿主应用的target中,找到PasswordCell的引用并删除。如下:

Paste_Image.png

在targets中选中AppExtesnionKit,为其添加Password.xib的引用,如下:

Paste_Image.png

操作完后,xib文件从原来bundle下的路径变成了bundle下AppExtensionKit下的路径。

做完这些还不够,我们还要在ExtensionDemo和PasswordAppExtension两个target下的Copy Bundle Resources中将AppExtensionKit导入进来,否则宿主应用和应用扩展还是用不了PasswordCell.xib。如图:

ExtensionDemo的target:

Paste_Image.png

PasswordAppExtension的target:

Paste_Image.png

那我们再次加载Password.xib文件,就需要从Bundle下的AppExtensionKit文件中加载。
加载方式代码如下:

cell = [[NSBundle mainBundle] loadNibNamed:@"AppExtensionKit.framework/ExtensionCell" owner:nil options:nil].lastObject;
Paste_Image.png

运行项目,效果如图:

2.pic.jpg

和宿主应用显示的数据一模一样。

自此,宿主应用与应用扩展的数据共享就完成了。

接下来,是Host App和应用扩展之间的数据传递。

Host App

Host App界面实现和代码逻辑都比较简单。

实现效果如下:

11111.gif

代码部分:
点击按钮时会触发如下代码:

Paste_Image.png Paste_Image.png

这里有几个关键点:
(1)首先,我创建了一个字典并且保存了两个参数,一个是版本号,一个是URLKey(我要将这个参数传递给应用扩展,应用扩展会用这个key做为查询条件到数据库中查询数据,然后将查询到的数据再回传给host app)。

(2)我把这个字典赋值给了NSItemProvider的item属性,又将NSItemProvider对象添加到了NSExtensionItem对象的attachments数组中。在应用扩展中,我们也按照这种方式来逐步获取字典。

(3)前面说过,系统提供了KUTTypeImage等字段用来在应用扩展中获取来自host app传递过来的值,而这个字段我们是可以自定义的。如图,这个自定义字段也是通过NSItemProvider对象来传递的。

Paste_Image.png

(3)在应用扩展中,我们如何通过这个自定义字段来获取host app传递过来的数据。如图:

Paste_Image.png

关键代码已经用红色方框标注出来了。

也就是说通过这句代码我们可以获取到host app向应用传递的typeIdentifier。这两个地方要一致才能获取到host app传递过来的数据。

在block回调中把host app传递过来的数据取出来,然后到数据库中进行查询就可以了。

(4)数据查询到了怎么回传给host app呢?
刚才已经展示过了应用扩展的界面,应用扩展实现了与宿主应用的数据共享。如图:


2.pic.jpg

当点击右上角关闭按钮时,什么数据都不回传。
当点击某个cell时,把对应的数据(也就是某条密码)回传给Host App,并把该密码的账户和密码显示在对应的输入框中。

代码如下:
关闭按钮的点击事件:

Paste_Image.png

单元格点击事件:

Paste_Image.png

到这里,应用扩展对host app的数据回传就搞定了。

(5)host app拿到回传数据进行登录

Paste_Image.png

这一步是通过UIActivityViewController对象的回调完成的。

不管是把数据从host app传给应用扩展,
还是把数据从应用扩展传给host app,
数据的传递依靠的都是NSExtensionItem和NSItemProvider,
如果非要给他们弄一个关系便于理解的话,大概是这样的:
存:
需要传递的数据 -> NSItemProvider -> NSExtensionItem -> NSExtensionContext
取:
NSExtensionContext -> NSExtensionItem -> NSItemProvider -> 拿到需要传递的数据
一层一层的包裹着。

OK,全部搞定。

我们来看一下最终的效果。

fdfas.gif

额,还差一点。

没有给我们的应用扩展配置一个图标。

Snip20160808_212.png Snip20160808_214.png Snip20160808_215.png

OK,全部搞定。

需求实现了。

但是在使用扩展的过程中还是有不少的坑,为了谨慎起见,在扩展中编写代码调用方法,多看看文档。有很多方法都有官方注释,有些方法是不能在应用扩展中使用的。

好,今天就到这里。

其他应用扩展资料传送门

上一篇下一篇

猜你喜欢

热点阅读