什么是shareExtension?
shareExtension蘋果在iOS8后開放給用戶使用,俗稱分享擴(kuò)展是應(yīng)用擴(kuò)展的一種(包括:分享擴(kuò)展,Today擴(kuò)展、Action擴(kuò)展、鍵盤擴(kuò)展等等),分享擴(kuò)展允許開發(fā)者擴(kuò)展應(yīng)用的自定義功能和內(nèi)容,能夠讓用戶在使用其他app時(shí)使用該項(xiàng)功能。擴(kuò)展不是一個(gè)可以獨(dú)立使用的應(yīng)用,它必須依附在一個(gè)應(yīng)用上才能發(fā)揮作用,有點(diǎn)像一個(gè)動(dòng)態(tài)庫,所有的app都可以使用,需要時(shí)系統(tǒng)會(huì)調(diào)用這個(gè)擴(kuò)展,臨時(shí)搭建一個(gè)環(huán)境來完成一些事情,完成后系統(tǒng)終止該擴(kuò)展的運(yùn)行。

一.創(chuàng)建一個(gè)shareExtension


二.配置shareExtension
創(chuàng)建shareExtension目錄之后,會(huì)出現(xiàn)4個(gè)文件其中
- ShreViewController 是默認(rèn)的分享界面,如果要自定義參考:Linky Adds a More Powerful Share Sheet to iOS 8
- MainInterface.storyboard 是默認(rèn)的storyboard。如果用代碼寫UI,則可刪除(有坑)
- Info.plist : 里面的版本號(hào)必須要和主工程的版本號(hào)一致,否則審核可能被拒。NSExtension非常重要,它決定你擴(kuò)展在什么情況出現(xiàn), 什么情況消失。比如我們的工程是最多只允許圖片5張+視頻5個(gè),超出后將在分享菜單項(xiàng)上看不到,可以這樣設(shè)置:

更多的設(shè)置可以點(diǎn)擊:Information Property List Key Reference
注意:通過不同的App打開分享擴(kuò)展,獲得的數(shù)據(jù)類型可能是不同的,比如<照片>里獲取的是圖片和視頻,<safari>里獲取的是URL文本。所以最好設(shè)置我們能處理的類型,以免出現(xiàn)異常
三.數(shù)據(jù)共享
App和擴(kuò)展應(yīng)用之間不能相互調(diào)用,它們有獨(dú)立的目錄結(jié)構(gòu):
分別在我們App和擴(kuò)展里分別執(zhí)行:NSLog(@"%@",NSHomeDirectory();
shareExtension的目錄: /var/mobile/Containers/Data/PluginKitPlugin/B5B809A4-F160-48F1-9AB2-2E9093EF0AA4我們自己App目錄: /var/mobile/Containers/Data/Application/7D38FDD2-C738-49EC-A9CC-1AA5E545E93C
iOS應(yīng)用存在一個(gè)沙盒里,不允許應(yīng)用之間進(jìn)行數(shù)據(jù)的交互,shareExtension也是一個(gè)具有獨(dú)立的Bundle Identifier的App。為此,蘋果提供了一項(xiàng)叫App Groups的服務(wù),該服務(wù)允許開發(fā)者可以在自己的應(yīng)用之間通過NSUserDefaults、NSFileManager或者CoreData來進(jìn)行相互的數(shù)據(jù)傳輸。下面介紹如何激活A(yù)pp Groups服務(wù):
- 首先申請(qǐng)一個(gè)App ID

- 增加一個(gè)App Groups

- 給App ID分配一個(gè)App Groups


也為我們自己App分配同樣的App Groups
給剛剛創(chuàng)建好的App ID生成一個(gè)Profile


- 回到XCode 在將shareExtension的和我們自己App的Capabilities 里把App Groups選項(xiàng)打開
image.png
至此已為shareExtension創(chuàng)建了一個(gè)bundle id 并且為shareExtension和我們App分配了一個(gè)App Groups。
下面分別介紹一下通過NSUserDefaults以及NSFileManager是如何實(shí)現(xiàn)App Groups下的數(shù)據(jù)操作:
- NSUserDefaults:要想設(shè)置或訪問Group的數(shù)據(jù),不能在使用standardUserDefaults方法來獲取一個(gè)NSUserDefaults對(duì)象了。應(yīng)該使用initWithSuiteName:方法來初始化一個(gè)NSUserDefaults對(duì)象,其中的SuiteName就是創(chuàng)建的Group的名字,然后利用這個(gè)對(duì)象來實(shí)現(xiàn),跨應(yīng)用的數(shù)據(jù)讀寫,代碼如下:
//初始化一個(gè)供App Groups使用的NSUserDefaults對(duì)象
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.pingan.shareExtension"];
//寫入數(shù)據(jù)
[userDefaults setValue:@"value" forKey:@"key"];
//讀取數(shù)據(jù)
NSLog(@"%@", [userDefaults valueForKey:@"key"]);
- NSFileManager:通過調(diào)用 containerURLForSecurityApplicationGroupIdentifier:方法可以獲得AppGroup的共享目錄,然后在此目錄的基礎(chǔ)上實(shí)現(xiàn)任意的文件操作(包括數(shù)據(jù)庫,文件歸檔等等)。代碼如下:
//獲取分組的共享目錄
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.pingan.shareExtension"];
NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"demo.txt"];
//寫入文件
[@"abc" writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];
//讀取文件
NSString *str = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:nil];
NSLog(@"str = %@", str);
四.獲取用戶選擇圖片&視頻
在viewDidLoad時(shí)就先把用戶選擇的圖片和視頻數(shù)據(jù)重新保存到共享區(qū)。不能根據(jù)系統(tǒng)提供的URL來使用(因?yàn)樗辉诠蚕韰^(qū),我們App無法識(shí)別該路徑,導(dǎo)致無法讀?。?/p>
- (void)viewDidLoad
{
[super viewDidLoad];
for (NSExtensionItem *item in self.extensionContext.inputItems) {
for (NSItemProvider *provider in item.attachments) {
if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePNG])
{
[provider loadItemForTypeIdentifier:(NSString *)kUTTypePNG
options:nil
completionHandler:^(NSURL * item, NSError * error) {
[self handleURL:item type:1];
}];
} else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeJPEG]){
[provider loadItemForTypeIdentifier:(NSString *)kUTTypeJPEG
options:nil
completionHandler:^(NSURL * item, NSError * error) {
[self handleURL:item type:1];
}];
} if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMPEG4]){
[provider loadItemForTypeIdentifier:(NSString *)kUTTypeMPEG4
options:nil
completionHandler:^(NSURL * item, NSError * error) {
[self handleURL:item type:3];
}];
} else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeQuickTimeMovie]){
[provider loadItemForTypeIdentifier:(NSString *)kUTTypeQuickTimeMovie
options:nil
completionHandler:^(NSURL * item, NSError * error) {
[self handleURL:item type:3];
}];
} else{
NSLog(@"未處理類型=========================================:%@", item);
}
}
}
}
- (void)handleURL:(NSURL *)item type:(NSInteger)type
{
//保存到共享空間
SEMessageItem *m = [SEMessageItem new];
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.pingan.klpa.shareExtension"];
NSURL *fileURL = [groupURL URLByAppendingPathComponent:[item lastPathComponent]];
NSString *name = [@"thumb_" stringByAppendingString:[item lastPathComponent]];
NSURL *fileThumbURL = [groupURL URLByAppendingPathComponent:name];
NSData *data = [NSData dataWithContentsOfURL:item];
m.bitSize = data.length;
if (type == 1) {//是圖片就壓縮一下,視頻暫時(shí)不壓縮
UIImage *image = [UIImage imageWithData:data];
data = UIImageJPEGRepresentation(image, 0.3);
m.mediaSize = image.size;
m.bitSize = data.length;
m.dataThumbURLPath = fileURL;
} else{
UIImage *image = [self getPreViewImg:item];
NSData *fdata = UIImagePNGRepresentation(image);
[fdata writeToURL:fileThumbURL atomically:NO];
m.dataThumbURLPath = fileThumbURL;
}
m.dataURLPath = fileURL;
[data writeToURL:fileURL atomically:YES];
m.cType = type;
[self.files addObject:m];
}
將類歸檔到文件中:
+ (NSArray <SEMessageItem *> *)readFromShareZone
{
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.pingan.klpa.shareExtension"];
NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"iphone_ex"];
NSData *myData = [NSData dataWithContentsOfURL:fileURL];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:myData];
NSArray *array = [unarchiver decodeObjectForKey:@"filesList"];
[unarchiver finishDecoding];
return array;
}
+ (void)write:(NSArray <SEMessageItem *> *)items
{
//獲取分組的共享目錄
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.pingan.klpa.shareExtension"];
NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"iphone_ex"];
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:items forKey:@"filesList"];
[archiver finishEncoding];
[data writeToURL:fileURL atomically:YES];
}
五.上傳文件和發(fā)送消息
現(xiàn)在我們需要做一個(gè)類似于微信,通過分享擴(kuò)展來實(shí)現(xiàn)發(fā)送文件的功能。 在我們的App設(shè)計(jì)中,將文件上傳到服務(wù)器,需要登陸成功后服務(wù)器返回的一個(gè)sessionid ,通過這個(gè)sessionid獲取一個(gè)token,再用這個(gè)token返回的信息來上傳文件。上傳完文件后發(fā)送一條聊天消息。所以分享擴(kuò)展需要解決上傳圖片和發(fā)送消息這兩點(diǎn)。
上傳圖片:因?yàn)閟hareExtension是一個(gè)簡(jiǎn)單的App,蘋果希望它只處理簡(jiǎn)單的邏輯,不喚起App就可以完成一些任務(wù)。所以不可能去做登錄UI類似的復(fù)雜邏輯,那不登錄又能獲得sessionid呢?答案就是通過App Groups,在容器App每次登錄后將sessionid保存到共享區(qū),擴(kuò)展程序再去讀取。如果讀取為空,那么就是未登錄或者是sessionId過期,這個(gè)時(shí)候需要在分享擴(kuò)展里提示用戶去容器App登錄
發(fā)送消息:通過和勝欽老司機(jī)討論,發(fā)現(xiàn)我們程序竟然有通過HTTP發(fā)送消息的接口,而且也是只需要sessionId就行。這令我喜出望外(__) ,不然又要把XMPP建立長(zhǎng)連接那一套搬過來(能不能搬過來還不好說,這工作量絕對(duì)是巨大的)。但是后來又發(fā)現(xiàn)一個(gè)問題,那就是發(fā)送的內(nèi)容需要xml格式的報(bào)文,這意味著XML組裝報(bào)文那一套需要搬過來,這個(gè)工作量也是巨大的,而且擴(kuò)展程序需要盡量保持簡(jiǎn)介,所以又經(jīng)過老司機(jī)的指點(diǎn)直接寫死:
//將需要的參數(shù)直接傳進(jìn)去
- (NSString *)getXMLWithContent:(NSString *)content
contentType:(NSInteger)cType
msgType:(NSInteger)mType
toJID:(NSString *)to
msgID:(NSString *)msgId
{
NSString *chatType = @"chat";
if (mType == 1) {
chatType = @"groupchat";
}
NSString *fromID = [self.myJID stringByAppendingString:@"@pingan.com.cn"];
long long int createCST = [[NSDate date]timeIntervalSince1970] *1000.0f;
NSString *string = @"<message type=\"%@\" to=\"%@\" id=\"%@\" from=\"%@/moiphone\"><body>%@</body><thread>m6RU90</thread><properties xmlns=\"http://www.jivesoftware.com/xmlns/xmpp/properties\"><property><name>createCST</name><value>%lld</value></property><property><name>contentType</name><value>%ld</value></property><property><name>totalTime</name><value/></property><property><name>retransmit</name><value>0</value></property><property><name>sourceMsg</name><value/></property><property><name>msgType</name><value>%ld</value></property></properties></message>";
NSString *formated = [NSString stringWithFormat:string, chatType, to, msgId, fromID, content, createCST, (long)cType, (long)mType];
return formated;
}
這樣只需要引入網(wǎng)絡(luò)框架和一些加密庫,就能在分享擴(kuò)展里順利的實(shí)現(xiàn)文件上傳和解決發(fā)送消息的問題。
注意這邊有個(gè)坑,在ShareViewController中沒有處理完你的任務(wù)之前是不能調(diào)用 [self.extensionContext completeRequestReturningItems:@[] completionHandler: nil]; 不然會(huì)直接退出擴(kuò)展程序,使文件上傳中斷
- (void)didSelectPost {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
for (SEMessageItem *item in self.files) {
//模擬向某人發(fā)送單聊消息
item.to = @"userId";
item.mType = 0;
}
[self.dataProvider savePhotosToContainerApp:self.files];
[self.dataProvider sendFilesWithMessages:self.files block:^(NSError *error) {
[SEMessageItem write:self.files];
//下面這句話會(huì)結(jié)束shareExtension, 所以要等所有事情做完才能調(diào)用這句話
[self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
}];
}
六.將數(shù)據(jù)同步到容器App
由于在分享擴(kuò)展程序里發(fā)送了圖片和視頻,需要同步到容器App的聊天會(huì)話中。在同步時(shí)發(fā)現(xiàn),界面有時(shí)卡很久。后來發(fā)現(xiàn)是因?yàn)橥ㄟ^擴(kuò)展應(yīng)用發(fā)送很多圖片和視頻,這里有大量的讀文件和寫數(shù)據(jù)庫操作,所以會(huì)耗時(shí)比較久,所以就開了個(gè)線程異步的去寫數(shù)據(jù)庫。其實(shí)sqlite還是同步的去寫,只不過將等待的過程挪到了線程里去,避免主線程卡死:
-(void)RSALoginSuccess:(NSDictionary *)successData withTag:(int)tag
{
// do something
//添加share extension 支持
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.pingan.klpa.shareExtension"];
[userDefaults setValue:userInfo forKey:kLastLoginUserInfo];
NSArray *files = [SEMessageItem readFromShareZone];
[self addMessageForContainerApp:files];
});
// do something
}
- (void)addMessageForContainerApp:(NSArray<SEMessageItem*> *)files
{
for (SEMessageItem *item in files) {
PAIMMessageModel *model = [[PAIMMessageModel alloc]init];
model.msgProto = PROTO_SEND;
model.msgTo = item.to;
model.msgFrom = [PAIMTools getMyJIDUser];
model.contentType = item.cType;
model.content = item.dataURLPath.relativePath;
model.totalSize = [NSString stringWithFormat:@"%d", (item.bitSize / 1024)];
model.msgType = item.mType;
model.createCST = item.createCST;
model.state = MESSAGE_SUCCESS;
NSString *limit = [item.to stringByAppendingString:kConversationIDSuffixTimed];
model.conversationID = [NSString PAIMMD5StringFrom:item.bLimitChat?limit:item.to];
model.groupID = item.to;
model.read = MESSAGE_READ;
model.thumbnailPic = item.dataThumbURLPath.relativePath;
model.messageType = item.bLimitChat;
model.msgId = item.msgID;
[PAIMMsgDBManager saveMessage:model];
}
}
七.編譯運(yùn)行



