打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
iOS 8 分享扩展(Share Extension)入门

前言

学习了一天扩展,除了官方文档还找了其他几篇文章来入门,链接都在文章下方,发现自己学习起来很有点费劲,我尝试写个更简单易懂的入门版本。学习扩展的初衷在于前天需要实现分享照片的功能,发现实现本身是很简单,同时发现还有其他方法来实现分享,本着紧跟苹果脚步学习新技术提升能力的美好想法,开始了学习。

首先要明白扩展的用途,对于分享扩展,它能提供自定义的分享服务,比如收集一段文字,一个网页链接,几张照片或是视频上传到支持的网站,等等,是个主流的网站都被系统支持这么干了吧,twitter, Facebook, weibo, tencentWeibo,你再这么干还有个啥意义;难道你要上传到你自己的网站,这除了哇一下还有什么值得惊喜的。我想了想,最大的意义就是可以自定义吧,耶。好吧,我明白了我前天那个仅仅是分享照片的需求用不着 iOS 8 的这个新功能,本次的学习是给其他类型的扩展打基础。分享扩展似乎是所有扩展里最没意义的一个了吧。看过的几篇文章都是使用上传功能作为范例,也许还有很多网站没有被系统支持但是提供了接口来实现上传功能,哦耶。一个国外程序员的分享扩展的作品:Linky Adds a More Powerful Share Sheet to iOS 8。看了这个有了更多的想法,之前的想法太局限了,或者说见识太少。分享扩展的最大用处是可以自定义。是的,分享到 twitter 等主流社交网站都被系统支持了,但利用这个分享扩展可以做出更好的分享功能以及 UI。可以说,系统开放的地方,开发者都能做得更好,当然这也是第三方该做的事。

好吧,分享扩展是个扩展,而扩展是很强大的:扩展不仅仅可以给自身的应用使用,还可以提供给其他应用使用。前天分享照片的需求里,如果需要提供自定义的其他分享服务,系统本身不支持,怎么办?可以使用UIActivity来提供自定义服务。可是我在其他应用里希望能够这个服务,怎么办?扩展能够帮助你,这就是扩展的强大之处:应用提供的服务通过扩展也能被其他应用使用。

对我来说,学习一个不知道有什么用的新特性总有种热情不够的感觉,学习算法的时候也是如此,虽然现在我悔之晚矣,当然,主要是找工作受挫。在我的学习过程中,没有那么一个契机来让我知道算法的重要性又或是随机数学、统计等学科的重要性。也许这只是我学习主动性不强的借口罢了。好了,进入正题。

扩展的工作机制

从昨天到现在,发现老老实实把官方文档看三遍就能明白扩展是干嘛的以及能干嘛和不能干嘛:App Extension Programming Guide。当然,鉴于本文的定位,我通俗地解释一下。
首先,扩展并不是一个独立的应用,它必须依附在一个应用上才能发挥作用,行为有点类似语法里的 Block,用户在一堆选项中选中扩展后,系统会调用这个扩展,临时搭建一个环境来完成一些事情,完成后系统终止该扩展的运行。

下图来自官方文档,解释一下,扩展与应用的关系:提供扩展的应用称之为容器应用(Containing app),比如你开发了一款 App,同时开发了扩展,那么你的 App 就是扩展的容器应用;而调用扩展的应用称之为载体应用(Host app),我觉得这个翻译比宿主应用要好,来自该文的翻译。当然,扩展也可以在容器应用中使用,这时容器应用同时也是载体应用。载体应用发出请求,系统替应用调用扩展,选择扩展后,扩展能够根据载体应用请求中的信息进行回应。


扩展与应用的关系

简单实现

扩展可以与载体应用直接通信,而不能和容器应用直接通信,需要通过共享数据来通信,因此才有了那几篇文章上来就进行 App Group 设置。但是,并不一定需要共享数据才能在容器应用中使用扩展,没有也可以的,等需要的时候再设置也不迟啊,而且往往还会出错,stackoverflow 上好几个 App Group 设置的问题,菜鸟如我,只是想尽快看到分享扩展的效果而已,结果看的几篇文章里,上来一通设置 App Group,制作 framework 在扩展和应用之间共享代码,直接懵了。

从上面的图来看,需要载体应用发出请求,然后系统会为应用调用扩展,扩展回应载体应用的请求。载体应用和扩展通信是通过 NSExtensionContext 对象来通信的,这么说来,需要我们在载体应用做点什么吗?答案是 NO,你不可能改变载体应用的行为,那么在容器应用里使用 UIActivityViewController 实现分享代码时,也不需要为扩展做出任何改变。系统会自动发出请求,而按照模板制作的扩展也不需要添加任何代码就可以在分享菜单中看到扩展了。虽然有几点需要注意,但是到了这一步已经足够建立起学习的信心了。接下来是介绍分享扩展的具体实现,使用模板来建立扩展,目前不支持自定义其他类型的扩展:

  1. 添加扩展的 target 到工程里,选择 File->New->Target,在界面面板左侧选择 Application Extension->Share Extension,将出现如下界面:

    添加分享扩展

    和新建工程类似,Product Name 将会是在分享菜单中显示的名字,不要太长,能够清晰标记应用就够了。OC 和 Swift 都支持,从学习的例子来看,Swift 语法的不稳定导致每个下载的例子都一堆编译错误,实在糟心。如果你熟悉 Swift,自然没问题,我选择 OC。一切保持默认设置即可。

    设置界面

    填写 Product Name,这里我填的是 AlbumsExtension,点击 Finish 后会出现激活对话框。

    激活

    激活后Xcode 的资源面板将会添加一个文件组,名字和你扩展名相同,包括:
    • 主类ShareViewController:继承自SLComposeServiceViewController,扩展的主类,既然是个 ViewController,意味着我们可以写普通的应用界面一样来构建扩展;
    • storyboard 文件:和应用一样,用于自定义界面。
    • info.plist:和应用里的 info.plist 一样用来配置扩展的行为,进一步的定制都在这里进行。
    • 在 Products 目录多了一个同名的 appex 文件,这是刚才添加的扩展的二进制打包文件。我之前已经添加了另外一个,对了,一个应用可以添加多个扩展。

      分享扩展模板的文件
  2. 现在为止已经具备实现一个扩展的条件了。接下来,编译,运行。

    编译 Scheme 选择

    在已有扩展的情况下,添加新的扩展好像已经直接添加到应用里了。但第一次运行扩展时,必须选择编译扩展的 scheme,因为扩展是依附于载体应用才能运行,编译时就需要选择载体应用,选择任意支持分享的应用都可以,但需要注意的选择其他非开发应用将无法调试扩展,只有选择容器应用才能设置断点进行调试。这里我选择容器应用以方便设置断点进行调试。

    载体应用 Host App 选择

    运行后,在支持功能处选择分享的界面如下,本次添加的 AlbumsExtension 在最下方,分享扩展与普通的分享项无异:

    展开 More 后的选项界面

    Enable 后,选择该扩展后的分享界面如下:

iPad 上分享扩展的界面

上面就是一个从模板建立一个分享扩展后不添加任何设置和代码所呈现的样子。但目前这个分享扩展什么都不能做,需要完善功能。

完善功能

载体应用与扩展通信是通过 NSExtensionContext 对象来通信的,在载体应用端不需要做额外的设置(你也不能做出修改),系统会自动为我们调用扩展,背后的通信机制不需要我们操心。在分享扩展里我们只需要针对收到的NSExtensionContext做出相应的操作即可。

分享扩展界面主要有三个功能点:检查输入内容的合法性,一般是检查字符数,你也可以加入其他限制条件;取消发布,取消按钮的代码不需要我们编写;发布功能,上传以及反馈载体应用,这里是最重要的一块。

分享扩展的主类是ShareViewController, 继承自SLComposeServiceViewController,是 iOS 8 中新增的类,提供标准化的社交分享视图,界面包括 textView, 剩余字符数指示器,支持对图像、视频的预览和有限设置。需要分享的信息可以从其extensionContext属性中的NSExtensionContext对象中获取,该对象接收自载体应用的请求。模板为我们提供了实现基本功能所需重写的方法,如下:

检查输入内容

- isContentValid:监测文本框的内容变化,输入文字时会调用该方法,返回 NO 时 Post 按钮不可用。从现实角度考虑,几乎所有的社交分享都会有字符数的限制,所以在这里设置,一旦超出限制,发布功能就不可用。SLComposeServiceViewController类的contentText属性用于跟踪文本输入框的内容, charactersRemaining 属性是NSNumber对象,并在文本输入框下方实时更新,在这个方法里更新剩余的字符数,以此判断是否超出字数限制。需要注意的是,不要使用NSUInteger,不然计算时下方的算式中会先转换成NSUInteger,小于 0 时会溢出。

static NSInteger const maxCharactersAllowed =  140;//手动设置字符数上限
- (BOOL)isContentValid {
// Do validation of contentText and/or NSExtensionContext attachments here
    NSInteger length = self.contentText.length;
    self.charactersRemaining = @(maxCharactersAllowed - length);
    if (self.charactersRemaining.integer < 0) {
        return NO;
    }
    return YES;
}
发布内容或取消

点击 Post 按钮后会调用-didSelectPost执行上传内容的操作;点击 Cancel 按钮后会调用-didSelectCancel取消分享。我们只需要实现这两个方法即可。

SLComposeServiceViewController类的extensionContext属性接收自载体应用请求,该属性是个NSExtensionContext对象无论是发布或是取消实质上都是使用该对象与载体应用通信:
1.- completeRequestReturningItems:completionHandler:
didSelectPost方法的最后调用,用于向载体应用反馈结果并且准备结束扩展的运行。一般来讲也没啥消息好反馈的。
2.- cancelRequestWithError:
didSelectCancel中调用,如果你需要反馈错误信息,可以重写该方法,一般来讲,告诉用户也没用,留着开发者自己调试用吧,所以也不必在ShareViewController中重写didSelectCancel方法。

官方文档建议点击发布后扩展界面立即消失,把上传任务放到后台。因为官方对扩展的定位是轻量化并且专注于单一功能,这样也有利于用户体验,我表示赞同。后台上传固然是好的,但对于让界面立即消失表示质疑。依我使用网络存储的经验,上传服务有着很大的不稳定性,往往速度不佳,上传内容不完整,苹果寄予厚望的 iCloud 云服务迄今为止表现依然很糟糕。可以说,智能手机的用户已经被糟糕的上传服务教育出来静静等待上传完毕的行为,立即消失等待后台完成后通知的行为相当没有安全感。在我的 iPad 上,iOS 8 里已有的分享选项里,印象笔记、Mail 这两项都只支持最多5张图片(支持数量和类型都可以设置,见下方内容),尽量减少上传的风险。虽然数量上的限制也许令人不爽,但能够使用分享扩展带来的便捷也许能让用户默默忍受。但如果上传过程 Block 界面让用户等待大概10秒,估计用户会立马禁掉该分享扩展,那么后台上传依然是最佳策略。

- (void)didSelectPost {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Upload asynchronously/后台异步上传内容
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
   NSArray *returnItems = ....;
   [self.extensionContext completeRequestReturningItems:returnItems completionHandler:nil];//向载体应用反馈结果并且让分享界面消失
}

- didSelectPost方法里的重头戏是建立一个后台上传任务。这个可以分解为两步:从extensionContext属性中提取数据(这一步不要在- didSelectPost里进行),利用 NSURLSession 类来建立后台任务。

提取数据

extensionContext是一个 NSExtensionContext 对象,分享的数据都封装在其inputItems属性中,这是一个包含 NSExtensionItem 对象的数组。实际上,不管你分享了多少内容,文字、多张图片或是链接,都封装在一个 NSExtensionItem 对象里,该对象中最重要的属性是attachments,是个NSItemProvider对象数组,每个 NSItemProvider 对象包含了单个分享的数据,可以是文字、图片、视频或是 URL。这些数据都由载体应用提供,使用 UIActivityViewController 来实现分享的时候传递的数据就封装在 NSItemProvider 对象里,也就是说实际上我们要从 NSItemProvider 对象里提取分享的数据。

NSItemProvider 类也是 iOS 8 中的新类,使用一种安全的方式在载体应用和 扩展之间传递数据,提供了以下两个方法来获取数据:
- (BOOL)hasItemConformingToTypeIdentifier:(NSString *)typeIdentifier//用于获取封装的数据类型,咋看之下非常别扭。需要提供 Uniform Type Identifier 简称 UTI 格式的标志符来找出数据类型。
- loadItemForTypeIdentifier:options:completionHandler://指定格式来获取数据,异步执行。

UTI是个什么玩意?主要是用于统一数据格式。想了解更多可以看官方文档:Introduction to Uniform Type Identifiers Overview


conformance_hierarchy.gif


哦,要了解的内容太多了点,现在先告诉我怎么指定格式吧。上图就是一些常用的格式,@"public.image"就是一个 UTI 标志符,可以用于- (BOOL)hasItemConformingToTypeIdentifier:(NSString *)typeIdentifier方法。但这个方法好笨,要一个个查询;还有另外一个办法,可以直接得到 NSItemProvider 对象中的数据格式,使用registeredTypeIdentifiers属性来获取数据格式,这是个数组,包含了对象中的所有格式。好吧,只有一种,但为什么要用数组来记录,难道为了日后一个 NSItemProvider 对象支持多个数据以及多数据类型,想不明白 API 为什么要设计得这么难用,或许是涉及 UTI 方面的考虑,是我想得太简单了,希望吧。得到数据格式后,使用- loadItemForTypeIdentifier:options:completionHandler:来获取数据,对于图像格式可以在 options 中指定图片大小,该方法在确定对象中确实拥有指定的格式后,会异步运行 completionHandler 闭包,该闭包第一个参数是个 id<NSSecureCoding> 对象,实际上需要你来指定数据类型。为了不影响分享界面的流畅性,在后台获取数据,具体代码如下:

- (void)fetchItemDataAtBackground
{
    //后台获取
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *inputItems = self.extensionContext.inputItems;
        NSExtensionItem *item = inputItems.firstObject;//无论多少数据,实际上只有一个 NSExtensionItem 对象
        for (NSItemProvider *provider in item.attachments) {
            //completionHandler 是异步运行的
            NSString *dataType = provider.registeredTypeIdentifiers.firstObject;//实际上一个NSItemProvider里也只有一种数据类型
            if ([dataType isEqualToString:@"public.image"]) {
                [provider loadItemForTypeIdentifier:dataType options:nil completionHandler:^(UIImage *image, NSError *error){
                    //collect image...
                }];
            }else if ([dataType isEqualToString:@"public.plain-text"]){
                [provider loadItemForTypeIdentifier:dataType options:nil completionHandler:^(NSString *contentText, NSError *error){
                    //collect image...
                }];
            }else if ([dataType isEqualToString:@"public.url"]){
                [provider loadItemForTypeIdentifier:dataType options:nil completionHandler:^(NSURL *url, NSError *error){
                    //collect url...
                }];
            }else
                NSLog(@"don't support data type: %@", dataType);
        }
    });
}

获取数据的行为不要放在- didSelectPost 里,这不用说明吧。在实际场景中,你可能会分享几张照片加上文字说明,或是 URL 链接加上文字说明,或是几个视频加上文字说明;总之基本上会有文字说明,而很少混合几种不同的类型,比如要同时分享照片、链接和视频,扩展是个轻量化的功能,混合类型让任务变得繁重,当然 PM 脑残允许这种方式的上传就看开发的心情了。

上传数据

在上一节中提取了数据后该上传了,这里不会给出一个具体的上传案例,可以参考 iOS 8 day by day 2 sharing extension 的例子在 requestb.in 这个网站上申请一个测试上传站点。

具体如何使用 NSURLSession 来上传,我就懒得写了,主要是麻烦。官方文档里也给了一个后台下载的例子。那么有什么需要注意的呢?这里将会涉及到开头提到的 App Group 设置。

数据共享

在我们点击 Post 按钮后,分享扩展调用了- didSelectPost方法,回顾一下上面的内容,扩展建立了一个后台任务后,通过 NSExtensionContext 来向载体应用反馈结果,然后就被系统终止运行了。那么这个后台任务完成后怎么办?总得有人负责处理呀。如果后台任务完成了,而发起任务的扩展已经被终止了,系统就会在后台唤醒扩展的容器应用来处理。这时候容器应用给扩展的任务收尾 ,需要知道数据。前面提过,容器应用和扩展没法直接通信,那么只能间接来了,建立共享数据容器。这个概念太抽象了,就是要建立容器应用和扩展都能操作的数据区域。


容器应用与扩展的数据共享

怎么建立呢?该 App Groups 上场了。App Groups 在应用和扩展之间定义一套标志符,只有拥有相同标志符的扩展和容器应用才能使用共享的数据。App Groups 的设置如下图,在容器应用中开启后,默认添加以 「group.」 开头的标志符,这些标志符都会登记到你的开发者账号中;同时目录中会多出一个和 target 名称相同的授权文件。再去对应的扩展的 Capabilities 页面下,刚才在容器应用下添加的标志符已经出现了,只需要勾选即可。


App Groups 设置

设置 App Groups 总是会遇到问题,在看那些开始就设置 App Groups 的文章时这点总让人很沮丧。我第一次设置遇到了签名问题,解决方案可以看这里:Certificate Problem for App Extension。有时候也回遇到一些自己就好了的问题,总之不让人舒心。

具体怎么使用 App Groups 中的标志符来共享数据呢?一些类支持配置App Groups标志符来在容器应用和扩展之间共享数据,此时不能使用常规设置。

  • NSUserDefaults:
      // Create and share access to an NSUserDefaults object
      NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.xxx"];
  • NSURLSession:
      NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@“xxx.backgroundsession”];
      // To access the shared container you set up, use the sharedContainerIdentifier property on your configuration object.
      config.sharedContainerIdentifier = @“group.xxx”;
      NSURLSession *mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    以上代码来自官方文档。
  • NSFileManager:
    [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.xxx"]
    该例子来自 《iOS 8 中 Extension 和 Containing App 之间的数据共享》

对于更复杂的数据共享,官方文档只写了这么一句:「Use Core Data, SQLite, or Posix locks to help coordinate data access in a shared container.」这篇博客对此进行了探索:《Share Core Data between app an extension in iOS》

对于一个后台上传任务,在 NSURLSession 上配置定义好的标志符,这样当容器应用接手时就能知道是哪个扩展的任务。此时在容器应用中需要做什么呢?当后台任务结束时,系统会调用-application:handleEventsForBackgroundURLSession:completionHandler:在后台唤醒容器应用来处理。

至此已经一个完善的分享功能已经完成。当然我没写上传的那部分。接下来,可以进行进一步的定制。

定制扩展行为

数据类型支持

分享扩展应该尽可能轻量化,这需要添加很多限制,比如限制分享类型、数量。当不满足条件时,分享扩展不会出现在分享菜单中,而不是仅仅没有开启。扩展的很多设置都可以在 info.plist 文件中实现。官方文档小节《Declaring Supported Data Types for a Share or Action Extension》给出了指导。
在扩展的 info.plist 文件中,找到 NSExtension 项,默认设置如下:


NSExtension 默认设置


NSExtensionActivationRule的默认类型是 String,其值 TRUEPREDICATE 表示在分享菜单上一直显示扩展。这种设置对分享不做任何限制,上面提到印象笔记和 Mail 应用只支持最多5张照片,超出后将在分享菜单项上看不到,如何做到的呢?在下图的设置中,将NSExtensionActivationRule的类型设置为 Dictionary,添加以下内容,扩展将仅支持最多5张图片,不支持 URL 和视频分享,如分享的对象不满足这个条件,分享菜单上该扩展会消失。


分享类型支持

注意:一旦你使用这种方式,没有出现在这里的数据类型将不会被支持,也就意味着该扩展不会出现;如果分享的数据不满足指定的数量限制,扩展也不会出现。以下是NSExtensionActivationRule支持的全部 Keys,参考链接在此


All keys for NSExtensionActivationRule

如果你想做出更复杂的过滤,可以使用NSPredicate语句来定制。老实说,我还不懂这个,有兴趣可以观看官方文档。

UI定制

目前UI中能够自定义的地方不多,可以做的有:

  1. 分享扩展不支持自定义 Icon,需要与容器应用保持一致。但可以自定义扩展显示名字:在 info.plist 里的Bundle display name项可以修改在分享菜单的扩展名字,这条来自 WWDC2014_205。
  2. 在文本输入框下支持类似 UITableViewCell 的配置项,需要在-configurationItems方法中返回一组 SLComposeSheetConfigurationItem 对象。印象笔记这里实现了支持选择不同的笔记本目录。
  3. placeholder属性用于在文本输入框内没有内容的占位内容。
  4. 可以修改 textView 上方标题栏的背景颜色,使用 navigationBar 来修改即可,但自定义 titleView 无法做到。本条来自这条回答

上面的都太简单了,分享扩展的 UI 可以完全自定义,一个国外程序员的分享扩展的作品 Linky Adds a More Powerful Share Sheet to iOS 8,UI 全部自定义,这差距。

其他

参考链接:
App Extension Programming Guide
iOS 通知中心扩展制作入门
iOS 8 day by day- day2: sharing extension 译文
iOS 8 中 Extension 和 Containing App 之间的数据共享
Share Core Data between app an extension in iOS

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
To B新品类观察||COP:未来企业阔步市场的创新容器和运营载体
iOS开发系列--App扩展开发
微信应用号申请
光「快」这一点,就足以让你用这款 App 了
OPPO R11新功能、快一点 大不同!
酷开5.1系统的主页自定义APP功能如何使用呢?
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服