iOS 之文件共享和iCloud创建文件夹

之前做项目铃声制作的时候,需求是创建app文件夹里面放去音频文件 可以复制到库乐队里面  当时研究这里的时候卡住了但是项目时间紧迫只能通过分享来添加到库乐队;项目审核后有点时间来研究这个问题 其实很简单先把需求放上去

我们得说到一个东西,叫做iOS 的文件目录 ,我们经常会对文件目录进行操作 ,我们熟悉以下(我们经常使用的)目录 

NSDocumentationDirectory

NSDocumentDirectory

NSDownloadsDirectory

NSCachesDirectory

为了观察我们分别建立四个不同的文件夹 。。。 

-(void)createDocumentationDirectory{

NSArray*paths = NSSearchPathForDirectoriesInDomains(NSDocumentationDirectory, NSUserDomainMask,YES);    

path1 = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"directoryOne"];

if(![fileManager fileExistsAtPath:path1]){        

[[NSFileManagerdefaultManager] createDirectoryAtPath:path1 withIntermediateDirectories:YESattributes:nilerror:nil];   

 }}

-(void)createDocumentDirectory{

NSArray*paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);  

  path2 = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"directoryTwo"];

if(![fileManager fileExistsAtPath:path2]){       

 [[NSFileManagerdefaultManager] createDirectoryAtPath:path2 withIntermediateDirectories:YESattributes:nilerror:nil];   

 }}

-(void)createDownloadsDirectory{

NSArray*paths = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask,YES);    

path3 = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"directoryThree"];

if(![fileManager fileExistsAtPath:path3]){      

  [[NSFileManagerdefaultManager] createDirectoryAtPath:path3 withIntermediateDirectories:YESattributes:nilerror:nil];   

 }}

-(void)createCachesDirectory{

NSArray*paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask,YES);  

  path4 = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"directoryFour"];

if(![fileManager fileExistsAtPath:path4]){       

 [[NSFileManagerdefaultManager] createDirectoryAtPath:path4 withIntermediateDirectories:YESattributes:nilerror:nil];    

}}

NSDocumentDirectory 只有这个才会出现文件夹


但是,我们还需要配置下 info.plist文件

Supports opening documents in place = YES

Application supports iTunes file sharing  =  YES


到这里才知道原来这是文件共享 之前还以为和iCloud有关系呢 查阅了一大堆的有关iCloud知识;既然是整理资料那就都放出来吧;

我们需要完成如下4件事情:

1.创建iCloud的容器,要求名字和id是唯一的。 iCloud容器名必须是唯一的,因为这是Cloudkit用来访问数据所使用的全局标识符。

为了让entitlements起作用,需要在App的证书、标识符与配置文件中ID的部分列出app/bundle id。这意味着标识的证书使用了设置的team id与app id,从中可得到iCloud容器的id。若已经在一个可用的开发者账号中标识了的话Xcode会自动完成这一切。不巧的是,这有时是不同步的,需要更新ID-使用iCloud功能面板修改CloudKit容器ID。否则的话需要修改info.plist文件或.entitlements文件来确保id values与所设置的bundle id一致。


2.创建支持iCloud的Apple ID,并关联上相应的iCloud容器。

创建的时候要勾上这里


完成之后还要Edit一下;


要和刚才的 iCloud关联一下;

3.创建、下载并安装支持iCloud的App对应的provisioning Profile,这个工作需要通过Apple 网站完成。



好 下面开始写代码了

正确姿势一 - 检测 iCloud 可用性

在使用 iCloud Document 之前,我们先要检测当前设备是否开启了 iCloud 功能,如果设备本身没有开启 iCloud,我们后续的操作就都会失败。 通过 NSFileManager 来获取 iCloud 的状态:

1NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)

这个方法接受一个参数, 就是要获取的容器标识。 所谓容器标识, 大多数应用只会用到一个 iCloud 容器,所以我们这里传入 nil, 就代表默认获取第一个可用的容器。

接下来,这个方法内部会查找当前应用拥有的 iCloud 容器, 如果找到就会返回这个容器的 URL, 证明当前应用的 iCloud 容器可用。 如果找不到,就会返回 nil, 证明当前应用的 iCloud 不可用。

这样我们就能根据这个方法的返回值来片段当前设备开启了 iCloud 服务。 只有在服务开启的时候,后续的操作才能进行。

这个方法获取的只是 iCloud 容器的根目录 URL, 我们大多数情况是不使用根目录的, 我们应该使用 Documents 目录, 所以这个方法还需要修改一下:

func getiCloudDocumentURL() -> NSURL? {

    iflet url = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil) {

        returnurl.URLByAppendingPathComponent("Documents")

    }

    returnnil

}

这样, 判断 iCloud 可用性以及获取目录 URL 的逻辑就都完成啦。 在实现具体逻辑的时候, 使用这个方法获取 URL, 如果能够获取,就可以进行下一步的文件列表操作了。 如果获取失败,就表示当前设备的 iCloud 服务不可用,或者当前 App 的 iCloud 服务没有开启, 这时候可以给用户一个提示, 去设置 iCloud。

最后一个小 Tip, iCloud 容器和你 App 文件沙盒, 在 iOS 文件系统中其实是分别存放在两个不同的地方的:

iCloud 文件路径格式 file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~xxx~aaa/Documents

App 沙盒文件路径格式 file:///var/mobile/Containers/Data/Application/3B4376B3-89B5-3342-8057-3450D4224518/Documents/

由此可见, 这也是为什么 iCloud 和 Sandbox 文件路径访问需要两套不同的方式的原因了。

正确姿势二 - 获取 iCloud 文件列表

iCloud 的另外一个陷阱就是文件列表的获取。 如果你有过 iOS 开发经验, 那么当得到了一个目录 URL 的时候, 你可能会想到这样得到目录中的文件列表:

iflet documentURL = getiCloudDocumentURL() {

    NSFileManager.defaultManager().contentsOfDirectoryAtURL(documentURL, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions.SkipsHiddenFiles)

}

从代码上看起来似乎没什么问题, 但如果你将这段代码用到 iCloud 文件的操作上, 很快你就会发现问题了。

这还要从 iCloud 在 iOS 系统上的运作机制说起。 其实 iCloud 的所有文件同步操作都是用过驻留在系统的一个进程进行的。 也就是说你的 App 所对应的 iCloud 目录,除了你的 App 进程会操作它, iCloud Daemon 也会操作它。 这就会带来并发访问资源的管理问题。

但这还不是全部,还有一个更好玩儿的。 假如你现在是用是 Mac 笔记本,那么其他设备只要向 iCloud 容器中添加新的文件,你的 iCloud Daemon 进程就会自动的将它们下载下来。

但在 iOS 系统中, iCloud Daemon 因为手机耗电以及网络流量等考虑, 是不会自动下载其他设备新添加到容器中的文件的。 只有你请求打开某个文件的时候才会去下载它的内容。

相信经过我这么一说,大家就察觉到问题了, 如果使用上面那种遍历目录的方法。 对于那些从其他设备添加,并且还没有下载到本地的文件,就会遍历不到了。很显然, 这不是我们期望的结果。

那么在 iOS 上面, 我们怎么取得完整的文件列表呢? iCloud 在 iOS 上虽然不会自动下载这些新添加的文件,但会将这些新文件的元信息(MetaData)传输过来,比如文件名,文件尺寸,修改时间等等。也就是说我们需要查询文件元信息的列表,就可以得到和服务端同步的文件列表了。

综上所述, 获取 iCloud 文件列表的正确姿势是这样:

let metaQuery = NSMetadataQuery()

func listFile() {

    metaQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]

    metaQuery.predicate = NSPredicate(value: true)

    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(listReceived), name:NSMetadataQueryDidFinishGatheringNotification, object: nil)

    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(listReceived), name:NSMetadataQueryDidUpdateNotification, object: nil)

    metaQuery.startQuery()

}

func listReceived() {

    let results = metaQuery.results

    foritem inresults {

        let fileURL = item.valueForAttribute(NSMetadataItemURLKey)

    }

    NSNotificationCenter.defaultCenter().removeObserver(self, name: NSMetadataQueryDidFinishGatheringNotification, object: nil)

    NSNotificationCenter.defaultCenter().removeObserver(self, name: NSMetadataQueryDidUpdateNotification, object: nil)

    metaQuery.stopQuery()

}

这段代码篇幅稍长, 首先我们初始化了一个 NSMetadataQuery 实例, 然后在 listFile 方法中设置它的属性。

metaQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] 这个属性表示我们要查询 iCloud 的 Documents 目录中的文件列表。

metaQuery.predicate = NSPredicate(value: true) 这个是对结果集的过滤选项, 我们这个 Query 默认接受所有文件。

NSMetadataQueryDidUpdateNotification 和 NSMetadataQueryDidFinishGatheringNotification 这两个通知分别表示得到查询数据的更新,以及得到全部查询数据。

接下来 listReceived 方法处理这两个通知, 这时候可以去到 metaQuery 的 results 属性,代表我们查找到的文件元信息列表, 最后使用 item.valueForAttribute(NSMetadataItemURLKey) 这样方法就可以得到包括文件 URL,文件尺寸,修改时间这些信息了。

在获取完相关的信息后, 我们可以调用 metaQuery.stopQuery() 方法结束查询操作。 并且 NSMetadataQuery 除了提供我们刚才这种一次性查询之外,还提供一个长期驻留查询的机制, 只要它的查询条件所覆盖的内容发生了变化,就会发送通知给我们。

但要注意一点, NSMetadataQuery 查询操作只能在我们 App 进入前台的时候开启, 也就是说当我们的 App 切换到后台的时候, 要记得暂停查询操作。

正确姿势三 - 使用 UIDocument

之前的文章中,我们讨论过一次关于 UIDocument 的内容,大家可以点击这里 回顾一下。

对于 iCloud 相关的文件操作,最好要使用 UIDocument 来进行。

为什么要使用 UIDocument 而不是直接通过文件操作 API 来进行呢? 这要从咱们刚才说到的进程间资源共享说起。

首先切记一点, iCloud 容器中的文件不止你的 App 在操作它。 还有另外一个叫做 iCloud Daemon 的家伙也在操作它。

这种多个进程共同操作一个资源的时候,就需要保证在同一时刻只有一个进程会操作这个资源。 如果两个进程同时操作这个资源,就会造成非常危险的后果。

比如你的 App 正在把你刚刚修改的内容写入一个文件, 而这个时候你的 iCloud Daemon 有可能将服务端对这个文件的改动也写入进来。 这样,你们最终的结果肯定会是其中一个操作覆盖了另一个操作。

这就需要一个同步机制, 当你的 App 进程在进行写入操作的时候, iCloud Daemon 会进行等待,当你写入完成后, 它才会将服务端的改动也同步过来。

当然了,上面这个简单例子只是为了让大家对资源的安全访问有一个直观的理解,在实际的情况要比我描述的这种更加复杂。

回到我们开始的讨论,类似 NSFileManager 这样的 API,是不能够保证多个进程之间这种安全访问机制的。 所以 iOS 引入了两个类 NSFileCoordinator 和 NSFilePresenter。

给大家一个直观的描述,假设有4个人同时操作一个文档, 每个人都会得到一个 NSFileCoordinator 和 NSFilePresenter。 假设其中一个人要给这个文档中加两行字,他先要用他自己的 NSFileCoordinator 发出通知给其他三个人。

其他 3 个人的 NSFilePresenter 会接收到这个通知,每个在这个时候都可以通过 NSFilePresenter 进行一些准备工作,当这些准备工作完成后,继续通过 NSFilePresenter 告诉通知的发起方,准备完成。

只要这三个人都发出了准备完成的通知后, 第一个发起者才能把这两行字写上去。

描述的比较直接~ 这也就是 NSFileCoordinator 和 NSFilePresenter 的基本原理,通过这个方式保证文件在多个进程键的访问安全。 关于这两个类的实际操作还会更复杂些,咱们在这里先做一个简要的了解。

iCloud 的官方文档中其实是强制要求使用者对文件的操作都通过 NSFileCoordinator 和 NSFilePresenter 来进行的。

但文件操作的逻辑其实很多, 而且这两个类的使用其实相对复杂, 如果不熟悉用错的话可能还会造成调试困难。所以基于这些原因,UIDocument 才浮出水面。这也是 UIDocument 最重要的好处。它的内部已经对 NSFileCoordinator 和 NSFilePresenter 做了封装,我们直接使用就好。

我们通过文件的 URL 即可初始化 UIDocument:

1let document = UIDocument(fileURL: fileURL)

初始化完成后, 我们直接打开即可:

document.openWithCompletionHandler { success in

    document.contents

}

UIDocument 会区分 Sanbox 和 iCloud 进行相应的处理, 并且处理多进程操作的问题。 调用完 openWithCompletionHandler 之后, 我们的 UIDocument 相当于已经打开的 NSFilePresenter, 如果其他进程要修改这个文件,我们就会接到通知,并进行准备工作, UIDocument 已经给我们提供了默认的实现。

当文档使用完毕后,可以调用:

document.closeWithCompletionHandler { success in

}

这个方法除了关闭文档之外,还会自动为我们处理文件保存操作,以及释放 NSFilePresenter 的占用。

最后, UIDocument 我们不能够直接使用, 还需要实现两个方法:

class Doc : UIDocument {

    varfileContents: String = "";

    override func contentsForType(typeName: String) throws -> AnyObject {

        returnfileContents.dataUsingEncoding(NSUTF8StringEncoding)!

    }

    override func loadFromContents(contents: AnyObject, ofType typeName: String?) throws {

        fileContents = NSString(data: contents as! NSData, encoding: NSUTF8StringEncoding) as! String

    }

}

UIDocument 虽然为我们实现了很多底层操作, 但如何获取文件内容的逻辑,还是留给了我们自己来实现。 contentsForType 和 loadFromContents 都是回调方法。 contentsForType 方法用于保存文件时提供给 UIDocument 要保存的数据, loadFromContents 用于 UIDocument 成功打开文件后,我们将数据解析成我们需要的文件内容,然后再保存起来。

之所以这样做, 我理解应该是 UIDocument 只是对通用文件的一个抽象。 可以是普通文本文件, 但也可以是其他类型的文件格式, 所以它传递给我们的就是一个原始的 data 数据,如何解析和处理这个数据,就交给了我们自己。

这里对 UIDocument 的基本使用给大家做了一个介绍, 更详细的使用方法大家就需要参考相关文档了。

结尾

自己也曾经开发过 iCloud 相关的功能, 刚开始总会莫名其妙的陷入一些陷阱当中, 出现莫名其妙的错误。 于是呢,花了几天时间好好研究了一下 iCloud 相关的文档。 在过程中发现 iCloud 的文档分布非常多, 从设计规范,到基于文档的编程规范,等等。散步在很多个主题文档中。所以只有把他们全都融汇起来才能慢慢的理解这套 API 背后的机制。

所以呢,这里我把我看到认为重要的地方做了一个梳理。也希望能够帮助大家少走弯路,抓住重点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,799评论 25 707
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,131评论 30 470
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32
  • 自认为我有好多面。 有时候觉得我很好,很努力,很乐于助人,很善良,很… 有时候又觉得自己很阴暗,总把别人想的那么坏...
    a1d2343b2d92阅读 225评论 0 0
  • 今天在项目中用drawRect画了一个三角型用于标示section是否折叠,如下图 当折叠的时候会有一个rotat...
    杰米阅读 2,185评论 0 0