iOS实现App之间的分享

我们在iOS平台上想要实现不同App之间的内容分享一般有几种常用方式:

  • 给每个App定义一个URL Scheme,通过访问指定了URL Scheme的一个URL,实现直接访问一个APP;
  • 通过UIDocumentInteractionController或者是UIActivityViewController这俩个iOS SDK中封装好的类在App之间发送数据、分享数据和操作数据;
  • 通过App Extension,在iOS 8的SDK中提供的扩展新特性实现跨App的数据操作和分享;

一、URL Scheme

给每个App定义一个URL Scheme,通过访问指定了URL Scheme的一个URL,实现直接访问一个APP;

//创建一个url,这个url就是跳转app的url,记得加上://
    NSURL *url = [NSURL URLWithString:@"otherApp://func?key=value&key1=value1&urlschemes= selfApp"];
    
    //先判断是否能打开该url
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
        //打开url
        [[UIApplication sharedApplication] openURL:url];
    } else {
        //给个提示或者做点别的事情
        NSLog(@"打不开otherApp://。请检查有没有设置URL Schemes白名单或者有没有安装带有otherApp://的应用");
    }   

这个方法只是创建了一个url,并且openURL。下面解析一下这个url的具体参数,canOpenURL:方法,在iOS9环境需要设置URL Schemes白名单。

  • 使用otherApp://这个URL Scheme来打开App
  • 使用func这个来判断具体跳转及相关操作等。
  • 使用key=value&key1=value1来传参
  • 使用urlschemes= selfApp来从其他app跳转回分享的app。

二、Share Extension

App Extension的介绍
官方给的说法是:App Extension可以让你扩展你的APP的自定义功能和内容,使用户可以在与其他应用或者系统进行互动的时候去使用它,
App Extension中Share Extension:分享扩展,发布一个共享网站或者与其他应用共享内容。

1、创建Share Extension扩展Target

然后选择”iOS” -> “Application Extension” -> “Share Extension”,点击“Next”。如图


1.png

2、配置Share Extension

接下来我们需要给他一些设置。我们展开XCode左侧栏的Share目录,找到Info.plist文件。如:


2.png

扩展Info.plist
我们只需要关注以下几个字段的设置

名称 说明
Bundle display name 扩展的显示名称,默认跟你的项目名称相同,可以通过修改此字段来控制扩展的显示名称。
NSExtension 扩展描述字段,用于描述扩展的属性、设置等。作为一个扩展项目必须要包含此字段。
NSExtensionAttributes 扩展属性集合字段。用于描述扩展的属性。
NSExtensionActivationRule 激活扩展的规则。默认为字符串“TRUEPREDICATE”,表示在分享菜单中一直显示该扩展。可以将类型改为Dictionary类型,然后添加以下字段:
NSExtensionActivationSupportsAttachmentsWithMaxCount
NSExtensionActivationSupportsAttachmentsWithMinCount
NSExtensionActivationSupportsImageWithMaxCount
NSExtensionActivationSupportsMovieWithMaxCount
NSExtensionActivationSupportsWebPageWithMaxCount
NSExtensionActivationSupportsWebURLWithMaxCount
NSExtensionMainStoryboard 设置主界面的Storyboard,如果不想使用storyboard,也可以使用NSExtensionPrincipalClass指定自定义UIViewController子类名
NSExtensionPointIdentifier 扩展标识,在分享扩展中为:com.apple.share-services
NSExtensionPrincipalClass 自定义UI的类名
NSExtensionActivationSupportsAttachmentsWithMaxCount 附件最多限制,为数值类型。附件包括File、Image和Movie三大类,单一、混选总量不超过指定数量
NSExtensionActivationSupportsAttachmentsWithMinCount 附件最少限制,为数值类型。当设置NSExtensionActivationSupportsAttachmentsWithMaxCount时生效,默认至少选择1个附件,分享菜单中才显示扩展插件图标。
NSExtensionActivationSupportsFileWithMaxCount 文件最多限制,为数值类型。文件泛指除Image/Movie之外的附件,例如【邮件】附件、【语音备忘录】等。

单一、混选均不超过指定数量。
NSExtensionActivationSupportsImageWithMaxCount 图片最多限制,为数值类型。单一、混选均不超过指定数量。
NSExtensionActivationSupportsMovieWithMaxCount 视频最多限制,为数值类型。单一、混选均不超过指定数量。
NSExtensionActivationSupportsText 是否支持文本类型,布尔类型,默认不支持。如【备忘录】的分享
NSExtensionActivationSupportsWebURLWithMaxCount Web链接最多限制,为数值类型。默认不支持分享超链接,需要自己设置一个数值。
NSExtensionActivationSupportsWebPageWithMaxCount Web页面最多限制,为数值类型。默认不支持Web页面分享,需要自己设置一个数值。

3、处理Share Extension中的数据

其实在Share Extension中默认都会有一个数据展现的UI界面ShareViewController。该界面继承SLComposeServiceViewController这个类型
其展现效果,如图:


3.png

顶部包括了标题、取消(Cancel)按钮和提交(Post)按钮。然后下面跟着左边就是一个文本编辑框,右边就是一个图片显示控件。那么,每当用户点击取消按钮或者提交按钮时,都会分别触发下面的方法:

/**
 *  点击取消按钮
 */
- (void)didSelectCancel
{
    [super didSelectCancel];
}

/**
 *  点击提交按钮
 */
- (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.
    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];
}


- (BOOL)isContentValid {
    // Do validation of contentText and/or NSExtensionContext attachments here
    
    NSExtensionItem * imageItem = [self.extensionContext.inputItems firstObject];
    if(!imageItem)
    {
        return NO;
    }
    NSItemProvider * imageItemProvider = [[imageItem attachments] firstObject];
    if(!imageItemProvider)
    {
        return NO;
    }
    if([imageItemProvider hasItemConformingToTypeIdentifier:@"public.url"]&&self.contentText)
    {
        return YES;
    }
 
    
    return YES;
}

- (void)viewDidLoad
{
    
    NSExtensionItem * imageItem = [self.extensionContext.inputItems firstObject];
    
    NSItemProvider * imageItemProvider = [[imageItem attachments] firstObject];
    
 
    
    if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString*)kUTTypeURL])
    {
        NSLog(@"xxxxxxxx");
        [imageItemProvider loadItemForTypeIdentifier:(NSString*)kUTTypeURL options:nil completionHandler:^(NSURL* imageUrl, NSError *error) {
            //在这儿做自己的工作
            NSLog(@"xxxxxxx123 = %@",imageUrl.absoluteString);
            urlString = imageUrl.absoluteString;
            
        }];
    }
 
    
}

自定义分享界面

如果通过扩展SLComposeServiceViewController还不能满足需求的情况下,这时候就需要自己设计一个分享视图控制器来替换默认的SLComposeServiceViewController

  1. 然后打开扩展的Info.plist文件,删除NSExtensionMainStoryboard属性并增加一项NSExtensionPrincipalClass属性并指向CustomShareViewController(注:这里没有使用Storyboard所以要删除该属性)

  2. 在 MainInterface.storyboard 重新拖拽一个新 UIViewController 即可,记得设置为初始 ViewController ,然后界面想怎么弄就怎么弄。

注:需要先走打开APP 再走self.extensionContext 否则会出现APP无法调起来的情况

在ShareExtension中,UIViewController包含一个extensionContext这样的上下文对象
NSExtensionContext的结构比较简单,包含一个属性和三个方法。其说明如下:

方法 说明
inputItems 该数组存储着容器应用传入给NSExtensionContext的NSExtensionItem数组。其中每个NSExtensionItem标识了一种类型的数据。要获取数据就要从这个属性入手。
completeRequestReturningItems:
completionHandler:
通知宿主程序的扩展已完成请求。调用此方法后,扩展UI会关闭并返回容器程序中。其中的items就是返回宿主程序的数据项。
cancelRequestWithError: 通知宿主程序的扩展已取消请求。调用此方法后,扩展UI会关闭并返回容器程序中。其中error为错误的描述信息。
NSExtensionItemsAndErrorsKey NSExtensionItem的userInfo属性中对应的错误信息键名。
从inputItems中获取数据

inputItems是包含NSExtensionItem类型对象的数组。那么,要处理里面的数据还得先来了解一下NSExtensionItem的结构

NSExtensionItem包含四个属性

属性 说明
attributedTitle 标题。
attributedContentText 内容。
attachments 附件数组,包含图片、视频、链接等资源,封装在NSItemProvider类型中。
userInfo 一个key-value结构的数据。NSExtensionItem中的属性都会在这个属性中一一映射。

对应userInfo结构中的NSExtensionItem属性的键名如下:

名称 说明
NSExtensionItemAttributedTitleKey 标题的键名
NSExtensionItemAttributedContentTextKey 内容的键名
NSExtensionItemAttachmentsKey 附件的键名
从上面的定义可以看出除了文本内容,其他类型的内容都是作为附件存储的,而附件又是封装在一个叫NSItemProvider的类型中,其定义如下:
方法 说明
initWithItem:typeIdentifier: 初始化方法,item为附件的数据,typeIdentifier是附件对应的类型标识,对应UTI的描述。
initWithContentsOfURL: 根据制定的文件路径来初始化。
registerItemForTypeIdentifier:loadHandler: 为一种资源类型自定义加载过程。这个方法主要针对自定义资源使用,例如自己定义的类或者文件格式等。当调用loadItemForTypeIdentifier:options:completionHandler:方法时就会触发定义的加载过程。
hasItemConformingToTypeIdentifier: 用于判断是否有typeIdentifier(UTI)所指定的资源存在。存在则返回YES,否则返回NO。
该方法结合loadItemForTypeIdentifier:options:completionHandler:使用。
loadItemForTypeIdentifier:options:completionHandler: 加载typeIdentifier指定的资源。加载是一个异步过程,加载完成后会触发completionHandler。
loadPreviewImageWithOptions:completionHandler: 加载资源的预览图片。
  • 打开容器应用的项目配置的Capabilities页签,激活App Groups特性
  • 容器程序启用AppGroup
    上述步骤完成后,容器程序的App Groups已经算是设置完成。然后轮到Share Extension插件需要激活App Groups服务,设置步骤跟容器程序相同,唯一不同的是,插件不需要创建新的App Group,只要加入到容器程序刚才创建的Group即可(这里可以理解为,哪些应用要实现共享数据,那么他们必须在同一个Group里面)。

至此,应用和扩展的App Groups服务都已经启动,现在就要进行分享内容的传输操作。下面分别介绍一下NSUserDefaults、NSFileManager以及CoreData三种方式是如何实现App Groups下的数据操作:

  • NSUserDefaults:要想设置或访问Group的数据,不能在使用standardUserDefaults方法来获取一个NSUserDefaults对象了。应该使用initWithSuiteName:方法来初始化一个NSUserDefaults对象,其中的SuiteName就是创建的Group的名字,然后利用这个对象来实现,跨应用的数据读写,代码如下:
//初始化一个供App Groups使用的NSUserDefaults对象
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.cn.vimfung.ShareExtensionDemo"];

//写入数据
[userDefaults setValue:@"value" forKey:@"key"];

//读取数据
NSLog(@"%@", [userDefaults valueForKey:@"key"]);
  • NSFileManager:通过调用 containerURLForSecurityApplicationGroupIdentifier:方法可以获得AppGroup的共享目录,然后在此目录的基础上实现任意的文件操作。代码如下:
//获取分组的共享目录
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cn.vimfung.ShareExtensionDemo"];
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);
  • CoreData:其实CoreData是基于NSFileManager取得共享目录后来实现数据共享的。即在初始化CoreData时,先使用NSFileManager取得共享目录,然后再指定共享目录为存储数据文件的目录(如存储的sqlite文件)。代码如下:
//获取分组的共享项目
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.cn.vimfung.ShareExtensionDemo"];
NSURL *storeURL = [containerURL URLByAppendingPathComponent:@"DataModel.sqlite"];

//初始化持久化存储调度器
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"DataModel" withExtension:@"momd"];

NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];

[coordinator addPersistentStoreWithType:NSSQLiteStoreType
                          configuration:nil
                                    URL:storeURL
                                options:nil
                                  error:nil];

//创建受控对象上下文
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

[context performBlockAndWait:^{
    [context setPersistentStoreCoordinator:coordinator];
}];

三、通过UTI让我们的App支持分享

UTI(Uniform Type Identifier),一套苹果给我们提供用来在基于Cocoa和Cocoa Touch应用程序中识别实体内容类型的规范,而关于实现内容关联的技术也正是基于这套规范。在iOS和Mac OS开发中,苹果给我们提供了注册文档类型的接口,而这种注册的文档类型是全局的,系统中所有的应用程序和服务都可以侦测到。因此我们通过这个底层侦测,可以使用其他可选的第三方App来预览我们的App中不支持的文档,而且我们还可以通过这个接口在我们的App中打开并处理第三方App的文档。

如果我们的App可以处理某些类型的实体内容,那么我们就可以在我们项目中的Info.plist文件中进行注册。当一个第三方App通过苹果的底层侦测技术检查有哪些App可以处理它所指定的内容类型时,如果我们的App已经注册了这种类型,那么我们的App图标就会显示在其中,并且作为我们自己的App的一个入口。

注册可用类型

我们需要在info.plist文件中,添加一个新的属性CFBundleDocumentTypes(实际上输入的是"Document types"),这是一个数组类型的属性,意思就是我们可以同时注册多个类型。而针对数组中的每一个元素,都有许多属性可以指定,详细的属性列表我们可以从官方文档上找到: Core Foundation Keys ---- CFBundleDocumentTypes。这里列举我们在做iOS开发时常用的属性:

  • CFBundleTypeName("Icon File Name")
    字符串类型,指定某种类型的别名,也就是用来指代我们规定的类型的别称,一般为了保持唯一性,我们使用UTI来标识。
  • CFBundleTypeIconFiles
    数组类型,包含指定的png图标的文件名,指定代表某种类型的图标,而图标有具体的尺寸标识:
Device Sizes
iPad 64 x 64 pixels, 320 x 320 pixels
iPhone and iPod touch 22 x 29 pixels, 44 x 58 pixels (high resolution)
  • LSItemContentTypes("Document Content Type UTIs")
    数组类型,包含UTI字符串,指定我们的应用程序所有可以识别的类型集合
  • LSHandlerRank("Handler rank")
    字符串类型,包含Owner,Default,Alternate,None四个可选值,指定对于某种类型的优先权级别,而Launcher Service会根据这个优先级别来排列显示的App的顺序。优先级别从高到低依次是OwnerAlternate,DefaultNone表示不接受这种类型。

程序回调
当我们通过上面步骤,成功地显示了我的app图标之后,点击图标,我们就可以跳转到我的应用中,而苹果在iOS SDK中给我们提供的接收回调的方法在iOS 9之后做出了改变,因此我们需要针对不同的设备版本做出改变:

4.png

5.png
6.png

iOS UTI (Universal Type Identifier) 通用类型标识符

  1. 同一类型标识符(Uniform Type Identifier,UTI)代表IOS信息共享的中心组件。可以把它看成下一代的MIME类型。UTI是标识资源类型(比如图像和文本)的字符串,他们制定哪些类型的信息将用于公共数据对象,他们不需要依赖于老式的指示符,比如文件扩展名,MIME类型,或者文件类型的元数据
image

如图,显示了Apple的基本顺应树的一部分。这个树上位于较低位置的任何项目都必须顺应其所有父数据属性。声明一个父UTI意味着支持他的所有子 UTI。因此,可以打开public.data的应用必须能打开文本,电影,图像文件等。其UTI的名称类型就是public.data等

  1. MIME的了解可以去百度百科上有定义:

MIME的定义类型如下 如text/xml就是后缀.xml的MIME类型。

常见的MIME类型(通用型):

超文本标记语言 文本 .html text/html

xml文档 .xml text/xml

  1. 常见的文件扩展名之间的相互转换

首先要添加MobileCoreServices.framework框架,并且在头文件中添加

#import

以下都用的是C语言编写的

(1)后缀名字符串转化为UTI字符串

-(NSString *)preferredUTIForExtention:(NSString *)ext
{
    //Request the UTI via the file extension
    NSString *theUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)(ext), NULL);
    return theUTI;
}

(2)使用kUITagClassMIMEType作为第一个参数,给UITypeCreatePreferredIdentifierForTag(),是MIME类型字符串转化为UTI字符串

NSString *preferredUTIForMIMEType(NSString *mime)
{
    //request the UTI via the file extention
    NSString *theUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType,(__bridge CFStringRef)mime, NULL);
    return theUTI;
}

(3)使用UITypeCopyPreferredTagWithClass(),是UTI字符串转化为后缀扩展名

NSString *extensionForUTI(NSString *aUTI)
{
    CFStringRef theUTI = (__bridge CFStringRef)aUTI;
    CFStringRef results = UTTypeCopyPreferredTagWithClass(theUTI, kUTTagClassFilenameExtension);
    return (__bridge_transfer NSString *)results;
}

(4)UTI字符串转化为MIME类型

NSString *mimeTypeForUTI(NSString *aUTI)
{
    CFStringRef theUTI = (__bridge CFStringRef) aUTI;
    CFStringRef results = UTTypeCopyPreferredTagWithClass(theUTI, kUTTagClassMIMEType);
    return (__bridge_transfer NSString *)results;
}

(5)测试顺应性,使用UITypeConformsTo()函数测试顺应性。该函数接受两个参数:一个源 UTI和一个要比较的UTI,如果第一个UTI顺应第二个UTI,就返回True。相等性测试则使用UITypeEqual(),下面显示了一个示例,说 明如何顺应性测试,确定文件路径是否可能指向图像资源。

BOOL pathPointsToLikelyUTIMatch(NSString *path, CFStringRef theUTI)
{
  NSString *extension = path.pathExtension;
  NSString *preferredUTI = preferredUTIForExtension(extension);
  return (UTTypeConformsTo((__bridge CFStringRef) preferredUTI, theUTI));
}
BOOL pathPointsToLikelyImage(NSString *path)
{
  return pathPointsToLikelyUTIMatch(path, CFSTR("public.image"));
}
BOOL pathPointsToLikelyAudio(NSString *path)
{
  return pathPointsToLikelyUTIMatch(path, CFSTR("public.audio"));
}

(6)获取顺应性列表

UTTypeCopyDeclaration()是IOS API中的所有UTI函数中最一般(并且最有用)的函数,它返回包含以下键的字典。

》kUTTypeIdentifierKey:UTI名称,他将被传递给函数(例如.public.mpeg)

》kUTTypeConformsToKey:类型顺应的任何父项目(例如 public.mpeg顺应public.movie)

》kUTTypeDescriptionKey:正在考虑的类型(如果存在的话)的现实描述 (例如 “MPEG movie”)

》kUTTypeTagSpecificationKey:给定UTI的等价OSType(例如MPG和MPEG)、文件扩展名( mpg、mpeg、mpe、m75和m15)和MIME类型(视频/mpeg、视频/mpg、视频/x-mpeg和视频/x-mpg)的字典。

下面例子主要是返回字典向上通过顺应性树来构建一个数组,表示给定UTI顺序的所有项目.例如public.mpeg类型顺应 public.movie public.audiovisual-content public.data public.item 和public.content,代码如下:

NSDictionary *utiDictionary(NSString *aUTI)
{
  NSDictionary *dictionary = (__bridge_transfer NSDictionary *)UTTypeCopyDeclaration((__bridge CFStringRef) aUTI);
  return dictionary;
}
NSArray *uniqueArray(NSArray *anArray)
{
  NSMutableArray *copiedArray = [NSMutableArray arrayWithArray:anArray];
  for (id object in anArray)
  {
    [copiedArray removeObjectIdenticalTo:object];
    [copiedArray addObject:object];
  }
  return copiedArray;
}
NSArray *conformanceArray(NSString *aUTI)
{
  NSMutableArray *results = [NSMutableArray arrayWithObject:aUTI];
  NSDictionary *dictionary = utiDictionary(aUTI);
  id conforms = dictionary[(__bridge NSString *)kUTTypeConformsToKey];
  // No conformance
  if (!conforms) return results;
  // Single conformance
  if ([conforms isKindOfClass:[NSString class]])
  {
    [results addObjectsFromArray:conformanceArray(conforms)];
    return uniqueArray(results);
  }
  // Iterate through multiple conformance
  if ([conforms isKindOfClass:[NSArray class]])
  {
    for (NSString *eachUTI in (NSArray *) conforms)
      [results addObjectsFromArray:conformanceArray(eachUTI)];
    return uniqueArray(results);
  }
  // Just return the one-item array
  return results;
}
NSArray *allExtensions(NSString *aUTI)
{
  NSMutableArray *results = [NSMutableArray array];
  NSArray *conformance = conformanceArray(aUTI);
  for (NSString *eachUTI in conformance)
  {
    NSDictionary *dictionary = utiDictionary(eachUTI);
    NSDictionary *extensions = dictionary[(__bridge NSString *)kUTTypeTagSpecificationKey];
    id fileTypes = extensions[(__bridge NSString *)kUTTagClassFilenameExtension];
    if ([fileTypes isKindOfClass:[NSArray class]])
      [results addObjectsFromArray:(NSArray *) fileTypes];
    else if ([fileTypes isKindOfClass:[NSString class]])
      [results addObject:(NSString *) fileTypes];
  }
  return uniqueArray(results);
}
NSArray *allMIMETypes(NSString *aUTI)
{
  NSMutableArray *results = [NSMutableArray array];
  NSArray *conformance = conformanceArray(aUTI);
  for (NSString *eachUTI in conformance)
  {
    NSDictionary *dictionary = utiDictionary(eachUTI);
    NSDictionary *extensions = dictionary[(__bridge NSString *)kUTTypeTagSpecificationKey];
    id fileTypes = extensions[(__bridge NSString *)kUTTagClassMIMEType];
    if ([fileTypes isKindOfClass:[NSArray class]])
      [results addObjectsFromArray:(NSArray *) fileTypes];
    else if ([fileTypes isKindOfClass:[NSString class]])
      [results addObject:(NSString *) fileTypes];
  }
  return uniqueArray(results);

你可能感兴趣的:(iOS实现App之间的分享)