之前做项目铃声制作的时候,需求是创建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 背后的机制。
所以呢,这里我把我看到认为重要的地方做了一个梳理。也希望能够帮助大家少走弯路,抓住重点。