什么是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个,超出后将在分享菜单项上看不到,可以这样设置:
更多的设置可以点击: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
- 增加一个App Groups
- 给App ID分配一个App Groups
-
也为我们自己App分配同样的App Groups
-
给刚刚创建好的App ID生成一个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];
}
}
网友评论