iOS Extension 拓展--從開發(fā)到發(fā)布全流程

背景

項(xiàng)目接入第三方支付泊碑,需要在三方應(yīng)用的分享面板的 Action 列表中顯示我們的 app缕溉,且跳轉(zhuǎn)到 app (containing app) 中,以上為需求背景待牵。

WechatIMG28528.png

如上圖,是對一張照片進(jìn)行分享操作喇勋,圖片中紅色部分就是 Share Extension 缨该,藍(lán)色部分是 Action Extension。

App Extension

1.什么是Extension
在 ios 8 之后 蘋果引入了全新的功能 App Extension,涉及方方面面 例如 Today 川背、keyBoard 贰拿、短信攔截、電話號碼過濾(之前一直想不明白熄云,短信和電話攔截是如何做到的)等等 二十多項(xiàng) Extension膨更。
Extension 就是字面意思 拓展 也可以認(rèn)為是插件,但不是你主 app (containing app) 的插件缴允,是你把一些功能做成了系統(tǒng)的插件荚守,比如攔截短信和電話、或上圖中的功能练般,是你在系統(tǒng)功能上做的插件矗漾。
Extension 和主 app (containing app) 之間是沒有直接關(guān)系,是兩個獨(dú)立的程序薄料,最直接的聯(lián)系就是 Extension 會跟隨主 app 的安裝一起安裝敞贡,卸載一起卸載。代碼不能相互調(diào)用摄职、存儲空間也不能相互訪問誊役。
但是,但是谷市,Extension 的功能是真的強(qiáng)大蛔垢,如果一旦了解 Extension 并且使用,就會打開新世界的大門歌懒。下面的以 Action Extension 這個為例啦桌,詳細(xì)介紹一下。

2.Extension如何工作
Extension 一般是在被其他 app 調(diào)起的,那這個 其他 app 被稱為 宿主應(yīng)用 (Host App
) 宿主應(yīng)用程序定義好了交流的上下文 extensionContext (下面會講到的NSItemProviderNSExtensionItem ) 然后調(diào)起 Extension甫男,然后 Extension 處理完宿主的請求任務(wù)之后且改,生命周期就結(jié)束了。

3.Extension 生命周期

WX20210923-173011.png

創(chuàng)建

創(chuàng)建一個普通的項(xiàng)目板驳,點(diǎn)擊 項(xiàng)目名稱又跛,在 target 列表下端選擇加號 添加 Extension:


extension_create.png
select action extension.png

根據(jù)提示正常輸入,我這里的名稱是 Action 最終點(diǎn)擊 finish 之后若治,就創(chuàng)建成功了慨蓝,之后的目錄結(jié)構(gòu)是下圖這樣:

WX20210923-142735@2x.png

紅框中的就是 Extension 的目錄結(jié)構(gòu)和 Target
ActionVierController 中進(jìn)行邏輯的開發(fā)工作,創(chuàng)建之后會默認(rèn)生成以下代碼,可以從代碼中看出基本的操作邏輯,遍歷 ExtensionContext.inputItems 端幼、在遍歷 NSExtensionItem 拿到 NSItemProvider 再然后 判斷 NSItemProvider 中對應(yīng)的 UTI (這個概念后面說) 代碼中 UTTypeImage.identifier 指的是圖片類型礼烈,說明當(dāng)前的 Action 邏輯只會處理圖片類型。再往下就是會主線程設(shè)置拿到的圖片就結(jié)束了婆跑。

- (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
字面意思 統(tǒng)一類型標(biāo)識此熬,Uniform type identifiers(UTIs) 提供了在整個系統(tǒng)里面標(biāo)識數(shù)據(jù)的一個統(tǒng)一的方式,比如 documents (文檔)滑进、pasteboard data (剪貼板數(shù)據(jù))和bundles (包)犀忱。

在使用系統(tǒng)分享的圖片的時候會看到 微信、支付寶扶关、淘寶等的 app icon阴汇,這是這些 app 在 share Extension 中設(shè)置的UTI 是支持圖片類型,Extension 具體支持響應(yīng)何種類型的數(shù)據(jù)节槐,會在 info.plist 中進(jìn)行設(shè)置搀庶,下面會講。

UTI 的定義和我們開發(fā) iOS 程序時填寫組織時一樣疯淫,采取的是反域名規(guī)則地来。如下面這幾種類型,同時 iOS 定義好了一些常用的UTI類型:

//自定義的
com.hk.hkicl

//機(jī)構(gòu) 公司定義好的
com.adobe.image

// 蘋果公司定義的
public.data
public.image
public.movie

UTI 有個一明顯的優(yōu)勢就是在一個順應(yīng)結(jié)構(gòu)中聲明的,簡單來說就是 UTI 是可以繼承的熙掺,如下圖:


WX20210923-153945.png

上圖中 public.html 繼承自 public.text 繼承自 public.data 所以如果我們明確知道我們想要操作的內(nèi)容是 HTML 格式的時候 我們使用 public.html 如果我們要操作的類型 包括 HTML /text/image等未斑,這樣我們就使用他們共同最近父類 public.data 就可以了。
上面只是簡單說了一下UTI 因?yàn)橄旅嫖覀円?Action 中使用進(jìn)行使用币绩,如果需要更加詳細(xì)蜡秽,后續(xù)我可以出一篇關(guān)于UTI的仔細(xì)講解。

Extension 設(shè)置 UTI
創(chuàng)建好的 Extension 目錄中 會有 info.plist 文件 來對應(yīng)的配置當(dāng)前 Extension,我們打開當(dāng)前 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>

其中關(guān)鍵字 NSExtensionActivationRule 是 Extension 的相應(yīng)規(guī)則
TRUEPREDICATE 代表 任意類型的數(shù)據(jù) 我都可以響應(yīng)到 (就比如: 我分享圖片里面有你芽突,我分享視頻里面有你,我分享文檔里面還有你董瞻,蘋果一看不得了啊 :兄弟寞蚌,你隔這兒當(dāng)海王疤锇汀! 嚇得蘋果趕緊發(fā)表聲明:你提交 release ipa 到 app store 的時候挟秤,不能是這個規(guī)則壹哺,不然我給丫拒了。)
所以正常情況下艘刚,這個規(guī)則是不能使用的需要進(jìn)行修改

<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>

這樣寫代表著 你支持的類型 和當(dāng)前一次最大可以操作的內(nèi)容的數(shù)量管宵。這樣的規(guī)則是提交沒有問題的。

自定義UTI
有一些情況攀甚,就比如 當(dāng)前我們的 app 只想在特定的UTI 情況下去響應(yīng)去操作箩朴,這種情況下就需要使用自定義的 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代表目前只操作數(shù)量為1炸庞。

題外話
如何調(diào)起特定 UTI 的 Extension 呢,下面貼一下代碼,方便大家做測試:

// 這里我們要使用到 NSItemProvider 對象静陈,用來封裝 分享的內(nèi)容
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 使用該對象進(jìn)行數(shù)據(jù)包裹燕雁,進(jìn)行數(shù)據(jù)的分享,就很容易理解在 Action Extension 中 ActionViewController 中 拆解數(shù)據(jù)的邏輯了鲸拥。
其中public.image 是我們使用 蘋果 提供的 UTI ,如有需要使用特定的 UTI 僧免,我們可以隨時更改 com.abc.def 這樣刑赶,就可以調(diào)起之前我們自定義的 UTI 的 Extension 了。

證書

Extension 的擴(kuò)展應(yīng)用同樣需要創(chuàng)建 bundleId 和下發(fā) profile 文件懂衩,這里說一下具體的步驟:

  1. 假如當(dāng)前 主 App (containing app) 的 bundleid 是 com.organization.app
  2. Extension 的 bundleId 則應(yīng)該是 com.organization.app.xx 的原則撞叨,在 主App 的 bundleId 后面拼一個名字,但是直接叫做 com.organization.app.extension 貌似是不行的浊洞。
  3. 同樣的是去 develop.apple.com 登錄賬號 創(chuàng)建 bundleId 然后制作 profile 文件牵敷。
  4. 如果需要實(shí)現(xiàn)數(shù)據(jù)和 主app 共享則需要在 bundleId 中打開數(shù)據(jù)共享開關(guān),添加和 主app 同樣的 groupid 即可法希,具體步驟會在下面的 數(shù)據(jù)共享中介紹枷餐,步驟一樣。

通信

因?yàn)?Extension 和 主app 是分別獨(dú)立的進(jìn)程苫亦,所以之間是不能直接通信的毛肋,但并不是沒有辦法實(shí)現(xiàn)通信。主要的辦法是通過 scheme 調(diào)用的方式屋剑,具體步驟:

  1. host app 即 宿主應(yīng)用 通過發(fā)起操作打開 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];
    }
}

數(shù)據(jù)共享

因?yàn)镋xtension 和 主App (containing app)是兩個相互獨(dú)立的進(jìn)程唉匾,所以是無法實(shí)現(xiàn)數(shù)據(jù)共享的孕讳,目前常用也是最好的方式 就是通過 GroupID 進(jìn)行數(shù)據(jù)共享。
對 bundleId 添加 groupID 功能,
首先 先去蘋果開發(fā)者中心 develop.apple.com 登錄厂财,然后修改 bundleId 的配置項(xiàng)芋簿,添加 groupId 功能,并且 創(chuàng)建 groupId 綁定到 bundleId 上:

WX20210923-175458.png

步驟 分別是 1蟀苛、2益咬、3、4 因?yàn)檫@邊我已經(jīng)創(chuàng)建好了groupId帜平,所以你們需要自己去創(chuàng)建幽告,然后保存,之后跟新 profile 文件裆甩,action Extension 的證書 我會在下面提到冗锁。
groupId 的格式為 group.bundleId 比如:group.com.organization.app
在 xcode 中 打開 groupId 的邏輯 即可

WX20210923-180155.png

上述操作是針對 主app (containing app) 。操作完成之后 就可以使用 共享數(shù)據(jù)模塊的功能了嗤栓,邏輯如下:

// 在 Extension 的進(jìn)行 數(shù)據(jù)存儲
 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 中 使用數(shù)據(jù) (主app 是用swift 寫的)
let shareDefaults = UserDefaults.init(suiteName: appGroupID);
shareDefaults?.synchronize();
if let paymentInfo = shareDefaults?.object(forKey: paymentInfoCacheKey) as? [String : String] {

這樣就實(shí)現(xiàn)了數(shù)據(jù)的共享操作冻河。

常見問題

1. 自定義 UTI 中 count == 1 的邏輯
在完成項(xiàng)目開發(fā)之后,當(dāng)前版本需要隱藏 Extension 功能茉帅,下個版本在發(fā)出去叨叙,這個時候,我天真的認(rèn)為 修改自定義邏輯中的 count == 1 改成 count == 0 就可以了堪澎,
結(jié)果發(fā)現(xiàn) 除了自定義的 UTI 和 iOS 定義的UTI 無法響應(yīng)之后擂错,其余任意 自定義 UTI 都可以響應(yīng)了。最好的做法 就是 先把 自定義的 UTI 用 UUID 代替樱蛤。
2. jenkins 打包中的錯誤
error: exportArchive: "XXX.appex" requires a provisioning profile with the App Groups feature.
用 jenkins 打 Distribution 包的時候除了這個錯誤钮呀,這個應(yīng)該是 打包 設(shè)置 ExportOptions.plist 文件配置的錯誤。
我在下面這篇內(nèi)容里面解釋一下這個問題:
http://www.reibang.com/p/b52c35ee8ac2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末昨凡,一起剝皮案震驚了整個濱河市爽醋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌便脊,老刑警劉巖蚂四,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異就轧,居然都是意外死亡证杭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門妒御,熙熙樓的掌柜王于貴愁眉苦臉地迎上來解愤,“玉大人,你說我怎么就攤上這事乎莉∷徒玻” “怎么了奸笤?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長哼鬓。 經(jīng)常有香客問我监右,道長,這世上最難降的妖魔是什么异希? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任健盒,我火速辦了婚禮,結(jié)果婚禮上称簿,老公的妹妹穿的比我還像新娘扣癣。我一直安慰自己,他們只是感情好憨降,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布父虑。 她就那樣靜靜地躺著,像睡著了一般授药。 火紅的嫁衣襯著肌膚如雪士嚎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天悔叽,我揣著相機(jī)與錄音莱衩,去河邊找鬼。 笑死娇澎,一個胖子當(dāng)著我的面吹牛膳殷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播九火,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼册招!你這毒婦竟也來了岔激?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤是掰,失蹤者是張志新(化名)和其女友劉穎虑鼎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體键痛,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡炫彩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了絮短。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片江兢。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖丁频,靈堂內(nèi)的尸體忽然破棺而出杉允,到底是詐尸還是另有隱情邑贴,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布叔磷,位于F島的核電站拢驾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏改基。R本人自食惡果不足惜繁疤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望秕狰。 院中可真熱鬧稠腊,春花似錦、人聲如沸封恰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诺舔。三九已至鳖昌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間低飒,已是汗流浹背许昨。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留褥赊,地道東北人糕档。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像拌喉,于是被迫代替她去往敵國和親速那。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355