美文网首页iOS开发知识点
iOS Extension 拓展--从开发到发布全流程

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

作者: 陈贤森 | 来源:发表于2021-09-24 11:38 被阅读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

    相关文章

      网友评论

        本文标题:iOS Extension 拓展--从开发到发布全流程

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