背景
項(xiàng)目接入第三方支付泊碑,需要在三方應(yīng)用的分享面板的 Action 列表中顯示我們的 app缕溉,且跳轉(zhuǎn)到 app (containing app) 中,以上為需求背景待牵。
如上圖,是對一張照片進(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
(下面會講到的NSItemProvider
和 NSExtensionItem
) 然后調(diào)起 Extension甫男,然后 Extension 處理完宿主的請求任務(wù)之后且改,生命周期就結(jié)束了。
3.Extension 生命周期
創(chuàng)建
創(chuàng)建一個普通的項(xiàng)目板驳,點(diǎn)擊 項(xiàng)目名稱又跛,在 target 列表下端選擇加號 添加 Extension:
根據(jù)提示正常輸入,我這里的名稱是 Action 最終點(diǎn)擊 finish 之后若治,就創(chuàng)建成功了慨蓝,之后的目錄結(jié)構(gòu)是下圖這樣:
紅框中的就是 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 是可以繼承的熙掺,如下圖:
上圖中 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 文件懂衩,這里說一下具體的步驟:
- 假如當(dāng)前 主 App (containing app) 的 bundleid 是
com.organization.app
- Extension 的 bundleId 則應(yīng)該是
com.organization.app.xx
的原則撞叨,在 主App 的 bundleId 后面拼一個名字,但是直接叫做com.organization.app.extension
貌似是不行的浊洞。 - 同樣的是去 develop.apple.com 登錄賬號 創(chuàng)建 bundleId 然后制作 profile 文件牵敷。
- 如果需要實(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)用的方式屋剑,具體步驟:
- 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 上:
步驟 分別是 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 的邏輯 即可
上述操作是針對 主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