iOS 开发shareExtension总结

作者: Hom_zhang | 来源:发表于2017-06-22 01:44 被阅读438次

什么是shareExtension?

shareExtension苹果在iOS8后开放给用户使用,俗称分享扩展是应用扩展的一种(包括:分享扩展,Today扩展、Action扩展、键盘扩展等等),分享扩展允许开发者扩展应用的自定义功能和内容,能够让用户在使用其他app时使用该项功能。扩展不是一个可以独立使用的应用,它必须依附在一个应用上才能发挥作用,有点像一个动态库,所有的app都可以使用,需要时系统会调用这个扩展,临时搭建一个环境来完成一些事情,完成后系统终止该扩展的运行。


通过<照片>启动微信的shareExtension

一.创建一个shareExtension

在XCode中选择File-New-Target,点击Finish 点击Activate将出现默认的文件夹,完成创建

二.配置shareExtension

创建shareExtension目录之后,会出现4个文件其中

  • ShreViewController 是默认的分享界面,如果要自定义参考:Linky Adds a More Powerful Share Sheet to iOS 8
  • MainInterface.storyboard 是默认的storyboard。如果用代码写UI,则可删除(有坑)
  • Info.plist : 里面的版本号必须要和主工程的版本号一致,否则审核可能被拒。NSExtension非常重要,它决定你扩展在什么情况出现, 什么情况消失。比如我们的工程是最多只允许图片5张+视频5个,超出后将在分享菜单项上看不到,可以这样设置:
image.png

更多的设置可以点击:Information Property List Key Reference

注意:通过不同的App打开分享扩展,获得的数据类型可能是不同的,比如<照片>里获取的是图片和视频,<safari>里获取的是URL文本。所以最好设置我们能处理的类型,以免出现异常

三.数据共享

App和扩展应用之间不能相互调用,它们有独立的目录结构:

分别在我们App和扩展里分别执行: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应用存在一个沙盒里,不允许应用之间进行数据的交互,shareExtension也是一个具有独立的Bundle Identifier的App。为此,苹果提供了一项叫App Groups的服务,该服务允许开发者可以在自己的应用之间通过NSUserDefaults、NSFileManager或者CoreData来进行相互的数据传输。下面介绍如何激活App Groups服务:

  • 首先申请一个App ID
image.png
  • 增加一个App Groups
image.png
  • 给App ID分配一个App Groups
点击Edit,分配刚刚创建的App Groups 绿色代表分配成功
  • 也为我们自己App分配同样的App Groups

  • 给刚刚创建好的App ID生成一个Profile

一路点下去,直到下面那个图 代表profile创建成功,下载下来双击即可
  • 回到XCode 在将shareExtension的和我们自己App的Capabilities 里把App Groups选项打开
    image.png

至此已为shareExtension创建了一个bundle id 并且为shareExtension和我们App分配了一个App Groups。

下面分别介绍一下通过NSUserDefaults以及NSFileManager是如何实现App Groups下的数据操作:

  • NSUserDefaults:要想设置或访问Group的数据,不能在使用standardUserDefaults方法来获取一个NSUserDefaults对象了。应该使用initWithSuiteName:方法来初始化一个NSUserDefaults对象,其中的SuiteName就是创建的Group的名字,然后利用这个对象来实现,跨应用的数据读写,代码如下:
  //初始化一个供App Groups使用的NSUserDefaults对象
  NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.pingan.shareExtension"];
  //写入数据
  [userDefaults setValue:@"value" forKey:@"key"];
  //读取数据
  NSLog(@"%@", [userDefaults valueForKey:@"key"]);
  • NSFileManager:通过调用 containerURLForSecurityApplicationGroupIdentifier:方法可以获得AppGroup的共享目录,然后在此目录的基础上实现任意的文件操作(包括数据库,文件归档等等)。代码如下:
  //获取分组的共享目录
  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时就先把用户选择的图片和视频数据重新保存到共享区。不能根据系统提供的URL来使用(因为它不在共享区,我们App无法识别该路径,导致无法读取)

- (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) {//是图片就压缩一下,视频暂时不压缩
        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];
}

五.上传文件和发送消息

现在我们需要做一个类似于微信,通过分享扩展来实现发送文件的功能。 在我们的App设计中,将文件上传到服务器,需要登陆成功后服务器返回的一个sessionid ,通过这个sessionid获取一个token,再用这个token返回的信息来上传文件。上传完文件后发送一条聊天消息。所以分享扩展需要解决上传图片和发送消息这两点。

  • 上传图片:因为shareExtension是一个简单的App,苹果希望它只处理简单的逻辑,不唤起App就可以完成一些任务。所以不可能去做登录UI类似的复杂逻辑,那不登录又能获得sessionid呢?答案就是通过App Groups,在容器App每次登录后将sessionid保存到共享区,扩展程序再去读取。如果读取为空,那么就是未登录或者是sessionId过期,这个时候需要在分享扩展里提示用户去容器App登录

  • 发送消息:通过和胜钦老司机讨论,发现我们程序竟然有通过HTTP发送消息的接口,而且也是只需要sessionId就行。这令我喜出望外(__) ,不然又要把XMPP建立长连接那一套搬过来(能不能搬过来还不好说,这工作量绝对是巨大的)。但是后来又发现一个问题,那就是发送的内容需要xml格式的报文,这意味着XML组装报文那一套需要搬过来,这个工作量也是巨大的,而且扩展程序需要尽量保持简介,所以又经过老司机的指点直接写死:

//将需要的参数直接传进去
  - (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;
}

这样只需要引入网络框架和一些加密库,就能在分享扩展里顺利的实现文件上传和解决发送消息的问题。

注意这边有个坑,在ShareViewController中没有处理完你的任务之前是不能调用 [self.extensionContext completeRequestReturningItems:@[] completionHandler: nil]; 不然会直接退出扩展程序,使文件上传中断

- (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) {
        //模拟向某人发送单聊消息
        item.to = @"userId";
        item.mType = 0;
    }
    
    [self.dataProvider savePhotosToContainerApp:self.files];
    [self.dataProvider sendFilesWithMessages:self.files block:^(NSError *error) {
        [SEMessageItem write:self.files];
        //下面这句话会结束shareExtension, 所以要等所有事情做完才能调用这句话
        [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
    }];
}

六.将数据同步到容器App

由于在分享扩展程序里发送了图片和视频,需要同步到容器App的聊天会话中。在同步时发现,界面有时卡很久。后来发现是因为通过扩展应用发送很多图片和视频,这里有大量的读文件和写数据库操作,所以会耗时比较久,所以就开了个线程异步的去写数据库。其实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];
    }
}

七.编译运行

选择对应的Target运行,然后选择运行这个扩展的App 在手机上通过扩展App发送一张图片,然后进入容器App查看,数据已经同步 对方收到一个图片消息

相关文章

网友评论

  • xing_x:系统点击post怎么跳转自己的应用啊
  • xing_x:你好,我按照你的步骤操作的,为什么在真机看不到自己的应用,只能在模拟器看到自己的应用,请问这是什么原因,急求
    烟花灬肆意:你好 问一下 我如何获取到好友列表来发送数据?
    xing_x:@Hom_zhang 找了,没有我的应用
    Hom_zhang:在选应用的那个界面, 你一直右滑,有一个更多, 点击更多配置

本文标题:iOS 开发shareExtension总结

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