iOS 開發(fā)shareExtension總結(jié)

什么是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)行。


通過<照片>啟動(dòng)微信的shareExtension

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

在XCode中選擇File-New-Target,點(diǎn)擊Finish
點(diǎn)擊Activate將出現(xiàn)默認(rèn)的文件夾,完成創(chuàng)建

二.配置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è)置:
image.png

更多的設(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
image.png
  • 增加一個(gè)App Groups
image.png
  • 給App ID分配一個(gè)App Groups
點(diǎn)擊Edit,分配剛剛創(chuàng)建的App Groups
綠色代表分配成功
  • 也為我們自己App分配同樣的App Groups

  • 給剛剛創(chuàng)建好的App ID生成一個(gè)Profile

一路點(diǎn)下去,直到下面那個(gè)圖
代表profile創(chuàng)建成功,下載下來雙擊即可
  • 回到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)行

選擇對(duì)應(yīng)的Target運(yùn)行,然后選擇運(yùn)行這個(gè)擴(kuò)展的App
在手機(jī)上通過擴(kuò)展App發(fā)送一張圖片,然后進(jìn)入容器App查看,數(shù)據(jù)已經(jīng)同步
對(duì)方收到一個(gè)圖片消息
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,581評(píng)論 30 472
  • Swift版本點(diǎn)擊這里歡迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh閱讀 25,990評(píng)論 7 249
  • 初到武夷山,入住齊云峰的一個(gè)會(huì)所后,稍事休息,老友李虹告訴我:韓哥,你今天來的真是時(shí)候,這個(gè)季節(jié)正是武夷山常常落雨...
    韓怡冰閱讀 452評(píng)論 0 0
  • 無它
    陽麗茗閱讀 142評(píng)論 0 0

友情鏈接更多精彩內(nèi)容