美文网首页傲视苍穹iOS《Objective-C》VIP专题OC开发开发
iOS Action Extension开发教程,实现跨APP的

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

作者: fou7 | 来源:发表于2016-08-08 18:10 被阅读3305次

    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,全部搞定。

    需求实现了。

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

    好,今天就到这里。

    其他应用扩展资料传送门

    相关文章

      网友评论

      • 190CM:你好 我为什么在创建扩展AppExtension完成 在添加库是看不到也搜索不到呢?
      • 男神已认证:[[[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.testAppExtension"] absoluteString] stringByAppendingPathComponent:@"TestDB.sqlite"]

        应该改成

        [[[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.testAppExtension"] path] stringByAppendingPathComponent:@"TestDB.sqlite"]
        xsr:确实是这样,换成path就可以。absoluteString path拿到的值也是一样的,但是区别在哪里呢?
      • Glang0:你好,楼主,请问下,我如何把qq里面的文件利用extension传到我自己的app中去?我看qq里面的extension有一些app可以传,这是什么原因啊?
        fou7:@梦相随Gg 没弄过,你可以去看看人家有没有提供技术文档。
      • James_Feng:可以分享一下demo吗:smile:
        fou7:@James_Feng 木有:disappointed_relieved:
      • 三上:Host app才应该翻译成宿主应用,Containing app可以翻译成容器应用,不然概念上很混乱。
        春眠不觉晓光:你理解的刚好反了吧?
      • 6091c07a7bb7:设置action icon的时候 general 里面没有 app Icon and launch 设置项 这时应该怎么办啊
      • 励志做大牛:很棒的帖子,楼主辛苦了,再接再厉 :+1:

      本文标题:iOS Action Extension开发教程,实现跨APP的

      本文链接:https://www.haomeiwen.com/subject/pmdlsttx.html