iOS Extension 拓展--从开发到发布全流程

背景

项目接入第三方支付,需要在三方应用的分享面板的 Action 列表中显示我们的 app,且跳转到 app (containing app) 中,以上为需求背景。

WechatIMG28528.png

如上图,是对一张照片进行分享操作,图片中红色部分就是 Share Extension ,蓝色部分是 Action Extension。

App Extension

1.什么是Extension
在 ios 8 之后 苹果引入了全新的功能 App Extension,涉及方方面面 例如 Today 、keyBoard 、短信拦截、电话号码过滤(之前一直想不明白,短信和电话拦截是如何做到的)等等 二十多项 Extension。
Extension 就是字面意思 拓展 也可以认为是插件,但不是你主 app (containing app) 的插件,是你把一些功能做成了系统的插件,比如拦截短信和电话、或上图中的功能,是你在系统功能上做的插件。
Extension 和主 app (containing app) 之间是没有直接关系,是两个独立的程序,最直接的联系就是 Extension 会跟随主 app 的安装一起安装,卸载一起卸载。代码不能相互调用、存储空间也不能相互访问。
但是,但是,Extension 的功能是真的强大,如果一旦了解 Extension 并且使用,就会打开新世界的大门。下面的以 Action Extension 这个为例,详细介绍一下。

2.Extension如何工作
Extension 一般是在被其他 app 调起的,那这个 其他 app 被称为 宿主应用 (Host App
) 宿主应用程序定义好了交流的上下文 extensionContext (下面会讲到的NSItemProviderNSExtensionItem ) 然后调起 Extension,然后 Extension 处理完宿主的请求任务之后,生命周期就结束了。

3.Extension 生命周期

WX20210923-173011.png

创建

创建一个普通的项目,点击 项目名称,在 target 列表下端选择加号 添加 Extension:


extension_create.png
select action extension.png

根据提示正常输入,我这里的名称是 Action 最终点击 finish 之后,就创建成功了,之后的目录结构是下图这样:

红框中的就是 Extension 的目录结构和 Target
ActionVierController 中进行逻辑的开发工作,创建之后会默认生成以下代码,可以从代码中看出基本的操作逻辑,遍历 ExtensionContext.inputItems 、在遍历 NSExtensionItem 拿到 NSItemProvider 再然后 判断 NSItemProvider 中对应的 UTI (这个概念后面说) 代码中 UTTypeImage.identifier 指的是图片类型,说明当前的 Action 逻辑只会处理图片类型。再往下就是会主线程设置拿到的图片就结束了。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Get the item[s] we're handling from the extension context.
    
    // For example, look for an image and place it into an image view.
    // Replace this with something appropriate for the type[s] your extension supports.
    BOOL imageFound = NO;
    for (NSExtensionItem *item in self.extensionContext.inputItems) {
        for (NSItemProvider *itemProvider in item.attachments) {
            if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeImage.identifier]) {
                // This is an image. We'll load it, then place it in our image view.
                __weak UIImageView *imageView = self.imageView;
                [itemProvider loadItemForTypeIdentifier:UTTypeImage.identifier options:nil completionHandler:^(UIImage *image, NSError *error) {
                    if(image) {
                        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                            [imageView setImage:image];
                        }];
                    }
                }];
                
                imageFound = YES;
                break;
            }
        }
        
        if (imageFound) {
            // We only handle one image, so stop looking for more.
            break;
        }
    }
}

- (IBAction)done {
    // Return any edited content to the host app.
    // This template doesn't do anything, so we just echo the passed in items.
    [self.extensionContext completeRequestReturningItems:self.extensionContext.inputItems completionHandler:nil];
}

UTI

Uninform Type Identifier
字面意思 统一类型标识,Uniform type identifiers(UTIs) 提供了在整个系统里面标识数据的一个统一的方式,比如 documents (文档)、pasteboard data (剪贴板数据)和bundles (包)。

在使用系统分享的图片的时候会看到 微信、支付宝、淘宝等的 app icon,这是这些 app 在 share Extension 中设置的UTI 是支持图片类型,Extension 具体支持响应何种类型的数据,会在 info.plist 中进行设置,下面会讲。

UTI 的定义和我们开发 iOS 程序时填写组织时一样,采取的是反域名规则。如下面这几种类型,同时 iOS 定义好了一些常用的UTI类型:

//自定义的
com.hk.hkicl

//机构 公司定义好的
com.adobe.image

// 苹果公司定义的
public.data
public.image
public.movie

UTI 有个一明显的优势就是在一个顺应结构中声明的,简单来说就是 UTI 是可以继承的,如下图:


WX20210923-153945.png

上图中 public.html 继承自 public.text 继承自 public.data 所以如果我们明确知道我们想要操作的内容是 HTML 格式的时候 我们使用 public.html 如果我们要操作的类型 包括 HTML /text/image等,这样我们就使用他们共同最近父类 public.data 就可以了。
上面只是简单说了一下UTI 因为下面我们要在 Action 中使用进行使用,如果需要更加详细,后续我可以出一篇关于UTI的仔细讲解。

Extension 设置 UTI
创建好的 Extension 目录中 会有 info.plist 文件 来对应的配置当前 Extension,我们打开当前 Action Extension 中的 info.plist 文件。



    NSExtension
    
        NSExtensionAttributes
        
            NSExtensionActivationRule
            TRUEPREDICATE
            NSExtensionServiceAllowsFinderPreviewItem
            
            NSExtensionServiceAllowsTouchBarItem
            
            NSExtensionServiceFinderPreviewIconName
            NSActionTemplate
            NSExtensionServiceTouchBarBezelColorName
            TouchBarBezel
            NSExtensionServiceTouchBarIconName
            NSActionTemplate
        
        NSExtensionMainStoryboard
        MainInterface
        NSExtensionPointIdentifier
        com.apple.ui-services
    


其中关键字 NSExtensionActivationRule 是 Extension 的相应规则
TRUEPREDICATE 代表 任意类型的数据 我都可以响应到 (就比如: 我分享图片里面有你,我分享视频里面有你,我分享文档里面还有你,苹果一看不得了啊 :兄弟,你隔这儿当海王啊! 吓得苹果赶紧发表声明:你提交 release ipa 到 app store 的时候,不能是这个规则,不然我给丫拒了。)
所以正常情况下,这个规则是不能使用的需要进行修改

NSExtensionAttributes
        
            NSExtensionActivationRule
            
                NSExtensionActivationSupportsAttachmentsWithMaxCount
                1
                NSExtensionActivationSupportsAttachmentsWithMinCount
                1
                NSExtensionActivationSupportsImageWithMaxCount
                1
                NSExtensionActivationSupportsMovieWithMaxCount
                1
                NSExtensionActivationSupportsWebPageWithMaxCount
                1
            
        

这样写代表着 你支持的类型 和当前一次最大可以操作的内容的数量。这样的规则是提交没有问题的。

自定义UTI
有一些情况,就比如 当前我们的 app 只想在特定的UTI 情况下去响应去操作,这种情况下就需要使用自定义的 UTI 了,写法如下:

NSExtensionAttributes
        
            NSExtensionActivationRule
            
                SUBQUERY (
                    extensionItems,
                    $extensionItem,
                    SUBQUERY (
                        $extensionItem.attachments,
                        $attachment,
                        ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.abc.def"
                    ).@count == $extensionItem.attachments.@count
                ).@count == 1
            
        

这种写法 中的 com.abc.def 就属于自定义的 UTI,同时外部的 count == 1代表目前只操作数量为1。

题外话
如何调起特定 UTI 的 Extension 呢,下面贴一下代码,方便大家做测试:

// 这里我们要使用到 NSItemProvider 对象,用来封装 分享的内容
NSString *url = @"http://www.baidu.com";
    UIImage *image = [UIImage imageNamed:@"image1.png"];
    NSItemProvider *provider = [[NSItemProvider alloc]
          initWithItem:@{@"URL" : url, @"image" : image, @"BACK" : @"http://www.sina.com"}
        typeIdentifier:@"public.image"];

    NSExtensionItem *item = [[NSExtensionItem alloc] init];
    item.accessibilityLabel = @"分享一二三";
    item.attachments = @[ provider ];

    UIActivityViewController *activityVC =
        [[UIActivityViewController alloc] initWithActivityItems:@[ item ]
                                          applicationActivities:nil];

从上诉代码中 我们可以看到 NSItemProvider 对象和 NSExtensionItem 使用该对象进行数据包裹,进行数据的分享,就很容易理解在 Action Extension 中 ActionViewController 中 拆解数据的逻辑了。
其中public.image 是我们使用 苹果 提供的 UTI ,如有需要使用特定的 UTI ,我们可以随时更改 com.abc.def 这样,就可以调起之前我们自定义的 UTI 的 Extension 了。

证书

Extension 的扩展应用同样需要创建 bundleId 和下发 profile 文件,这里说一下具体的步骤:

  1. 假如当前 主 App (containing app) 的 bundleid 是 com.organization.app
  2. Extension 的 bundleId 则应该是 com.organization.app.xx 的原则,在 主App 的 bundleId 后面拼一个名字,但是直接叫做 com.organization.app.extension 貌似是不行的。
  3. 同样的是去 develop.apple.com 登录账号 创建 bundleId 然后制作 profile 文件。
  4. 如果需要实现数据和 主app 共享则需要在 bundleId 中打开数据共享开关,添加和 主app 同样的 groupid 即可,具体步骤会在下面的 数据共享中介绍,步骤一样。

通信

因为 Extension 和 主app 是分别独立的进程,所以之间是不能直接通信的,但并不是没有办法实现通信。主要的办法是通过 scheme 调用的方式,具体步骤:

  1. host app 即 宿主应用 通过发起操作打开 Extension app ,可以再 Extension app 中拿到 host app 的UIApplication,通过 主app 的scheme 用openURL 打开, 具体代码如下:
- (void)openAirStarBankApp {
    NSURL *destinationURL = [NSURL URLWithString:appToAppScheme];
    NSString *className = [[NSString alloc] initWithData:[NSData dataWithBytes:(unsigned char []){0x55, 0x49, 0x41, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E} length:13] encoding:NSASCIIStringEncoding];
    if (NSClassFromString(className)) {
        id object = [NSClassFromString(className) performSelector:@selector(sharedApplication)];
        [object performSelector:@selector(openURL:) withObject:destinationURL];
    }
}

数据共享

因为Extension 和 主App (containing app)是两个相互独立的进程,所以是无法实现数据共享的,目前常用也是最好的方式 就是通过 GroupID 进行数据共享。
对 bundleId 添加 groupID 功能,
首先 先去苹果开发者中心 develop.apple.com 登录,然后修改 bundleId 的配置项,添加 groupId 功能,并且 创建 groupId 绑定到 bundleId 上:

WX20210923-175458.png

步骤 分别是 1、2、3、4 因为这边我已经创建好了groupId,所以你们需要自己去创建,然后保存,之后跟新 profile 文件,action Extension 的证书 我会在下面提到。
groupId 的格式为 group.bundleId 比如:group.com.organization.app
在 xcode 中 打开 groupId 的逻辑 即可

WX20210923-180155.png

上述操作是针对 主app (containing app) 。操作完成之后 就可以使用 共享数据模块的功能了,逻辑如下:

// 在 Extension 的进行 数据存储
 NSUserDefaults *shareDefaults = [[NSUserDefaults alloc] initWithSuiteName:appGroupID];
            
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
 [params setObject:paymentUrl forKey:paymentUrlKey];
            
NSString *paymentBackUrl = [dictionary objectForKey:paymentBackKey];
if (paymentBackUrl) {
    [params setObject:paymentBackUrl forKey:paymentBackKey];
}
 [shareDefaults setObject:params forKey:paymentInfoCacheKey];        
 [shareDefaults synchronize];

// 在主 app 中 使用数据 (主app 是用swift 写的)
let shareDefaults = UserDefaults.init(suiteName: appGroupID);
shareDefaults?.synchronize();
if let paymentInfo = shareDefaults?.object(forKey: paymentInfoCacheKey) as? [String : String] {

这样就实现了数据的共享操作。

常见问题

1. 自定义 UTI 中 count == 1 的逻辑
在完成项目开发之后,当前版本需要隐藏 Extension 功能,下个版本在发出去,这个时候,我天真的认为 修改自定义逻辑中的 count == 1 改成 count == 0 就可以了,
结果发现 除了自定义的 UTI 和 iOS 定义的UTI 无法响应之后,其余任意 自定义 UTI 都可以响应了。最好的做法 就是 先把 自定义的 UTI 用 UUID 代替。
2. jenkins 打包中的错误
error: exportArchive: "XXX.appex" requires a provisioning profile with the App Groups feature.
用 jenkins 打 Distribution 包的时候除了这个错误,这个应该是 打包 设置 ExportOptions.plist 文件配置的错误。
我在下面这篇内容里面解释一下这个问题:
https://www.jianshu.com/p/b52c35ee8ac2

你可能感兴趣的:(iOS Extension 拓展--从开发到发布全流程)