iOS开发知识点

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

2021-09-24  本文已影响0人  陈贤森

背景

项目接入第三方支付,需要在三方应用的分享面板的 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 之后,就创建成功了,之后的目录结构是下图这样:

WX20210923-142735@2x.png

红框中的就是 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 文件。

<plist version="1.0">
<dict>
    <key>NSExtension</key>
    <dict>
        <key>NSExtensionAttributes</key>
        <dict>
            <key>NSExtensionActivationRule</key>
            <string>TRUEPREDICATE</string>
            <key>NSExtensionServiceAllowsFinderPreviewItem</key>
            <true/>
            <key>NSExtensionServiceAllowsTouchBarItem</key>
            <true/>
            <key>NSExtensionServiceFinderPreviewIconName</key>
            <string>NSActionTemplate</string>
            <key>NSExtensionServiceTouchBarBezelColorName</key>
            <string>TouchBarBezel</string>
            <key>NSExtensionServiceTouchBarIconName</key>
            <string>NSActionTemplate</string>
        </dict>
        <key>NSExtensionMainStoryboard</key>
        <string>MainInterface</string>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.ui-services</string>
    </dict>
</dict>
</plist>

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

<key>NSExtensionAttributes</key>
        <dict>
            <key>NSExtensionActivationRule</key>
            <dict>
                <key>NSExtensionActivationSupportsAttachmentsWithMaxCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsImageWithMaxCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
                <integer>1</integer>
            </dict>
        </dict>

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

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

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

这种写法 中的 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

上一篇下一篇

猜你喜欢

热点阅读