之前做項(xiàng)目鈴聲制作的時(shí)候,需求是創(chuàng)建app文件夾里面放去音頻文件 可以復(fù)制到庫(kù)樂(lè)隊(duì)里面 ?當(dāng)時(shí)研究這里的時(shí)候卡住了但是項(xiàng)目時(shí)間緊迫只能通過(guò)分享來(lái)添加到庫(kù)樂(lè)隊(duì);項(xiàng)目審核后有點(diǎn)時(shí)間來(lái)研究這個(gè)問(wèn)題 其實(shí)很簡(jiǎn)單先把需求放上去

我們得說(shuō)到一個(gè)東西,叫做iOS 的文件目錄 ,我們經(jīng)常會(huì)對(duì)文件目錄進(jìn)行操作 ,我們熟悉以下(我們經(jīng)常使用的)目錄?
NSDocumentationDirectory
NSDocumentDirectory
NSDownloadsDirectory
NSCachesDirectory
為了觀察我們分別建立四個(gè)不同的文件夾 。。。?
-(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 只有這個(gè)才會(huì)出現(xiàn)文件夾

但是,我們還需要配置下 info.plist文件
Supports opening documents in place = YES
Application supports iTunes file sharing? =? YES
到這里才知道原來(lái)這是文件共享 之前還以為和iCloud有關(guān)系呢 查閱了一大堆的有關(guān)iCloud知識(shí);既然是整理資料那就都放出來(lái)吧;
我們需要完成如下4件事情:
1.創(chuàng)建iCloud的容器,要求名字和id是唯一的。?iCloud容器名必須是唯一的,因?yàn)檫@是Cloudkit用來(lái)訪問(wèn)數(shù)據(jù)所使用的全局標(biāo)識(shí)符。
為了讓entitlements起作用,需要在App的證書(shū)、標(biāo)識(shí)符與配置文件中ID的部分列出app/bundle id。這意味著標(biāo)識(shí)的證書(shū)使用了設(shè)置的team id與app id,從中可得到iCloud容器的id。若已經(jīng)在一個(gè)可用的開(kāi)發(fā)者賬號(hào)中標(biāo)識(shí)了的話Xcode會(huì)自動(dòng)完成這一切。不巧的是,這有時(shí)是不同步的,需要更新ID-使用iCloud功能面板修改CloudKit容器ID。否則的話需要修改info.plist文件或.entitlements文件來(lái)確保id values與所設(shè)置的bundle id一致。

2.創(chuàng)建支持iCloud的Apple ID,并關(guān)聯(lián)上相應(yīng)的iCloud容器。
創(chuàng)建的時(shí)候要勾上這里

完成之后還要Edit一下;

要和剛才的?iCloud關(guān)聯(lián)一下;
3.創(chuàng)建、下載并安裝支持iCloud的App對(duì)應(yīng)的provisioning Profile,這個(gè)工作需要通過(guò)Apple 網(wǎng)站完成。



好 下面開(kāi)始寫(xiě)代碼了
正確姿勢(shì)一 - 檢測(cè) iCloud 可用性
在使用 iCloud Document 之前,我們先要檢測(cè)當(dāng)前設(shè)備是否開(kāi)啟了 iCloud 功能,如果設(shè)備本身沒(méi)有開(kāi)啟 iCloud,我們后續(xù)的操作就都會(huì)失敗。 通過(guò) NSFileManager 來(lái)獲取 iCloud 的狀態(tài):
1NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
這個(gè)方法接受一個(gè)參數(shù), 就是要獲取的容器標(biāo)識(shí)。 所謂容器標(biāo)識(shí), 大多數(shù)應(yīng)用只會(huì)用到一個(gè) iCloud 容器,所以我們這里傳入 nil, 就代表默認(rèn)獲取第一個(gè)可用的容器。
接下來(lái),這個(gè)方法內(nèi)部會(huì)查找當(dāng)前應(yīng)用擁有的 iCloud 容器, 如果找到就會(huì)返回這個(gè)容器的 URL, 證明當(dāng)前應(yīng)用的 iCloud 容器可用。 如果找不到,就會(huì)返回 nil, 證明當(dāng)前應(yīng)用的 iCloud 不可用。
這樣我們就能根據(jù)這個(gè)方法的返回值來(lái)片段當(dāng)前設(shè)備開(kāi)啟了 iCloud 服務(wù)。 只有在服務(wù)開(kāi)啟的時(shí)候,后續(xù)的操作才能進(jìn)行。
這個(gè)方法獲取的只是 iCloud 容器的根目錄 URL, 我們大多數(shù)情況是不使用根目錄的, 我們應(yīng)該使用 Documents 目錄, 所以這個(gè)方法還需要修改一下:
func?getiCloudDocumentURL()?->?NSURL??{
????iflet?url?=?NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)?{
????????returnurl.URLByAppendingPathComponent("Documents")
????}
????returnnil
}
這樣, 判斷 iCloud 可用性以及獲取目錄 URL 的邏輯就都完成啦。 在實(shí)現(xiàn)具體邏輯的時(shí)候, 使用這個(gè)方法獲取 URL, 如果能夠獲取,就可以進(jìn)行下一步的文件列表操作了。 如果獲取失敗,就表示當(dāng)前設(shè)備的 iCloud 服務(wù)不可用,或者當(dāng)前 App 的 iCloud 服務(wù)沒(méi)有開(kāi)啟, 這時(shí)候可以給用戶(hù)一個(gè)提示, 去設(shè)置 iCloud。
最后一個(gè)小 Tip, iCloud 容器和你 App 文件沙盒, 在 iOS 文件系統(tǒng)中其實(shí)是分別存放在兩個(gè)不同的地方的:
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/
由此可見(jiàn), 這也是為什么 iCloud 和 Sandbox 文件路徑訪問(wèn)需要兩套不同的方式的原因了。
正確姿勢(shì)二 - 獲取 iCloud 文件列表
iCloud 的另外一個(gè)陷阱就是文件列表的獲取。 如果你有過(guò) iOS 開(kāi)發(fā)經(jīng)驗(yàn), 那么當(dāng)?shù)玫搅艘粋€(gè)目錄 URL 的時(shí)候, 你可能會(huì)想到這樣得到目錄中的文件列表:
iflet?documentURL?=?getiCloudDocumentURL()?{
????NSFileManager.defaultManager().contentsOfDirectoryAtURL(documentURL,?includingPropertiesForKeys:?nil,?options:?NSDirectoryEnumerationOptions.SkipsHiddenFiles)
}
從代碼上看起來(lái)似乎沒(méi)什么問(wèn)題, 但如果你將這段代碼用到 iCloud 文件的操作上, 很快你就會(huì)發(fā)現(xiàn)問(wèn)題了。
這還要從 iCloud 在 iOS 系統(tǒng)上的運(yùn)作機(jī)制說(shuō)起。 其實(shí) iCloud 的所有文件同步操作都是用過(guò)駐留在系統(tǒng)的一個(gè)進(jìn)程進(jìn)行的。 也就是說(shuō)你的 App 所對(duì)應(yīng)的 iCloud 目錄,除了你的 App 進(jìn)程會(huì)操作它, iCloud Daemon 也會(huì)操作它。 這就會(huì)帶來(lái)并發(fā)訪問(wèn)資源的管理問(wèn)題。
但這還不是全部,還有一個(gè)更好玩兒的。 假如你現(xiàn)在是用是 Mac 筆記本,那么其他設(shè)備只要向 iCloud 容器中添加新的文件,你的 iCloud Daemon 進(jìn)程就會(huì)自動(dòng)的將它們下載下來(lái)。
但在 iOS 系統(tǒng)中, iCloud Daemon 因?yàn)槭謾C(jī)耗電以及網(wǎng)絡(luò)流量等考慮, 是不會(huì)自動(dòng)下載其他設(shè)備新添加到容器中的文件的。 只有你請(qǐng)求打開(kāi)某個(gè)文件的時(shí)候才會(huì)去下載它的內(nèi)容。
相信經(jīng)過(guò)我這么一說(shuō),大家就察覺(jué)到問(wèn)題了, 如果使用上面那種遍歷目錄的方法。 對(duì)于那些從其他設(shè)備添加,并且還沒(méi)有下載到本地的文件,就會(huì)遍歷不到了。很顯然, 這不是我們期望的結(jié)果。
那么在 iOS 上面, 我們?cè)趺慈〉猛暾奈募斜砟兀?iCloud 在 iOS 上雖然不會(huì)自動(dòng)下載這些新添加的文件,但會(huì)將這些新文件的元信息(MetaData)傳輸過(guò)來(lái),比如文件名,文件尺寸,修改時(shí)間等等。也就是說(shuō)我們需要查詢(xún)文件元信息的列表,就可以得到和服務(wù)端同步的文件列表了。
綜上所述, 獲取 iCloud 文件列表的正確姿勢(shì)是這樣:
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()
}
這段代碼篇幅稍長(zhǎng), 首先我們初始化了一個(gè) NSMetadataQuery 實(shí)例, 然后在 listFile 方法中設(shè)置它的屬性。
metaQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] 這個(gè)屬性表示我們要查詢(xún) iCloud 的 Documents 目錄中的文件列表。
metaQuery.predicate = NSPredicate(value: true) 這個(gè)是對(duì)結(jié)果集的過(guò)濾選項(xiàng), 我們這個(gè) Query 默認(rèn)接受所有文件。
NSMetadataQueryDidUpdateNotification 和 NSMetadataQueryDidFinishGatheringNotification 這兩個(gè)通知分別表示得到查詢(xún)數(shù)據(jù)的更新,以及得到全部查詢(xún)數(shù)據(jù)。
接下來(lái) listReceived 方法處理這兩個(gè)通知, 這時(shí)候可以去到 metaQuery 的 results 屬性,代表我們查找到的文件元信息列表, 最后使用 item.valueForAttribute(NSMetadataItemURLKey) 這樣方法就可以得到包括文件 URL,文件尺寸,修改時(shí)間這些信息了。
在獲取完相關(guān)的信息后, 我們可以調(diào)用 metaQuery.stopQuery() 方法結(jié)束查詢(xún)操作。 并且 NSMetadataQuery 除了提供我們剛才這種一次性查詢(xún)之外,還提供一個(gè)長(zhǎng)期駐留查詢(xún)的機(jī)制, 只要它的查詢(xún)條件所覆蓋的內(nèi)容發(fā)生了變化,就會(huì)發(fā)送通知給我們。
但要注意一點(diǎn), NSMetadataQuery 查詢(xún)操作只能在我們 App 進(jìn)入前臺(tái)的時(shí)候開(kāi)啟, 也就是說(shuō)當(dāng)我們的 App 切換到后臺(tái)的時(shí)候, 要記得暫停查詢(xún)操作。
正確姿勢(shì)三 - 使用 UIDocument
之前的文章中,我們討論過(guò)一次關(guān)于 UIDocument 的內(nèi)容,大家可以點(diǎn)擊這里 回顧一下。
對(duì)于 iCloud 相關(guān)的文件操作,最好要使用 UIDocument 來(lái)進(jìn)行。
為什么要使用 UIDocument 而不是直接通過(guò)文件操作 API 來(lái)進(jìn)行呢? 這要從咱們剛才說(shuō)到的進(jìn)程間資源共享說(shuō)起。
首先切記一點(diǎn), iCloud 容器中的文件不止你的 App 在操作它。 還有另外一個(gè)叫做 iCloud Daemon 的家伙也在操作它。
這種多個(gè)進(jìn)程共同操作一個(gè)資源的時(shí)候,就需要保證在同一時(shí)刻只有一個(gè)進(jìn)程會(huì)操作這個(gè)資源。 如果兩個(gè)進(jìn)程同時(shí)操作這個(gè)資源,就會(huì)造成非常危險(xiǎn)的后果。
比如你的 App 正在把你剛剛修改的內(nèi)容寫(xiě)入一個(gè)文件, 而這個(gè)時(shí)候你的 iCloud Daemon 有可能將服務(wù)端對(duì)這個(gè)文件的改動(dòng)也寫(xiě)入進(jìn)來(lái)。 這樣,你們最終的結(jié)果肯定會(huì)是其中一個(gè)操作覆蓋了另一個(gè)操作。
這就需要一個(gè)同步機(jī)制, 當(dāng)你的 App 進(jìn)程在進(jìn)行寫(xiě)入操作的時(shí)候, iCloud Daemon 會(huì)進(jìn)行等待,當(dāng)你寫(xiě)入完成后, 它才會(huì)將服務(wù)端的改動(dòng)也同步過(guò)來(lái)。
當(dāng)然了,上面這個(gè)簡(jiǎn)單例子只是為了讓大家對(duì)資源的安全訪問(wèn)有一個(gè)直觀的理解,在實(shí)際的情況要比我描述的這種更加復(fù)雜。
回到我們開(kāi)始的討論,類(lèi)似 NSFileManager 這樣的 API,是不能夠保證多個(gè)進(jìn)程之間這種安全訪問(wèn)機(jī)制的。 所以 iOS 引入了兩個(gè)類(lèi) NSFileCoordinator 和 NSFilePresenter。
給大家一個(gè)直觀的描述,假設(shè)有4個(gè)人同時(shí)操作一個(gè)文檔, 每個(gè)人都會(huì)得到一個(gè) NSFileCoordinator 和 NSFilePresenter。 假設(shè)其中一個(gè)人要給這個(gè)文檔中加兩行字,他先要用他自己的 NSFileCoordinator 發(fā)出通知給其他三個(gè)人。
其他 3 個(gè)人的 NSFilePresenter 會(huì)接收到這個(gè)通知,每個(gè)在這個(gè)時(shí)候都可以通過(guò) NSFilePresenter 進(jìn)行一些準(zhǔn)備工作,當(dāng)這些準(zhǔn)備工作完成后,繼續(xù)通過(guò) NSFilePresenter 告訴通知的發(fā)起方,準(zhǔn)備完成。
只要這三個(gè)人都發(fā)出了準(zhǔn)備完成的通知后, 第一個(gè)發(fā)起者才能把這兩行字寫(xiě)上去。
描述的比較直接~ 這也就是 NSFileCoordinator 和 NSFilePresenter 的基本原理,通過(guò)這個(gè)方式保證文件在多個(gè)進(jìn)程鍵的訪問(wèn)安全。 關(guān)于這兩個(gè)類(lèi)的實(shí)際操作還會(huì)更復(fù)雜些,咱們?cè)谶@里先做一個(gè)簡(jiǎn)要的了解。
iCloud 的官方文檔中其實(shí)是強(qiáng)制要求使用者對(duì)文件的操作都通過(guò) NSFileCoordinator 和 NSFilePresenter 來(lái)進(jìn)行的。
但文件操作的邏輯其實(shí)很多, 而且這兩個(gè)類(lèi)的使用其實(shí)相對(duì)復(fù)雜, 如果不熟悉用錯(cuò)的話可能還會(huì)造成調(diào)試?yán)щy。所以基于這些原因,UIDocument 才浮出水面。這也是 UIDocument 最重要的好處。它的內(nèi)部已經(jīng)對(duì) NSFileCoordinator 和 NSFilePresenter 做了封裝,我們直接使用就好。
我們通過(guò)文件的 URL 即可初始化 UIDocument:
1let?document?=?UIDocument(fileURL:?fileURL)
初始化完成后, 我們直接打開(kāi)即可:
document.openWithCompletionHandler?{?success?in
????document.contents
}
UIDocument 會(huì)區(qū)分 Sanbox 和 iCloud 進(jìn)行相應(yīng)的處理, 并且處理多進(jìn)程操作的問(wèn)題。 調(diào)用完 openWithCompletionHandler 之后, 我們的 UIDocument 相當(dāng)于已經(jīng)打開(kāi)的 NSFilePresenter, 如果其他進(jìn)程要修改這個(gè)文件,我們就會(huì)接到通知,并進(jìn)行準(zhǔn)備工作, UIDocument 已經(jīng)給我們提供了默認(rèn)的實(shí)現(xiàn)。
當(dāng)文檔使用完畢后,可以調(diào)用:
document.closeWithCompletionHandler?{?success?in
}
這個(gè)方法除了關(guān)閉文檔之外,還會(huì)自動(dòng)為我們處理文件保存操作,以及釋放 NSFilePresenter 的占用。
最后, UIDocument 我們不能夠直接使用, 還需要實(shí)現(xiàn)兩個(gè)方法:
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 雖然為我們實(shí)現(xiàn)了很多底層操作, 但如何獲取文件內(nèi)容的邏輯,還是留給了我們自己來(lái)實(shí)現(xiàn)。 contentsForType 和 loadFromContents 都是回調(diào)方法。 contentsForType 方法用于保存文件時(shí)提供給 UIDocument 要保存的數(shù)據(jù), loadFromContents 用于 UIDocument 成功打開(kāi)文件后,我們將數(shù)據(jù)解析成我們需要的文件內(nèi)容,然后再保存起來(lái)。
之所以這樣做, 我理解應(yīng)該是 UIDocument 只是對(duì)通用文件的一個(gè)抽象。 可以是普通文本文件, 但也可以是其他類(lèi)型的文件格式, 所以它傳遞給我們的就是一個(gè)原始的 data 數(shù)據(jù),如何解析和處理這個(gè)數(shù)據(jù),就交給了我們自己。
這里對(duì) UIDocument 的基本使用給大家做了一個(gè)介紹, 更詳細(xì)的使用方法大家就需要參考相關(guān)文檔了。
結(jié)尾
自己也曾經(jīng)開(kāi)發(fā)過(guò) iCloud 相關(guān)的功能, 剛開(kāi)始總會(huì)莫名其妙的陷入一些陷阱當(dāng)中, 出現(xiàn)莫名其妙的錯(cuò)誤。 于是呢,花了幾天時(shí)間好好研究了一下 iCloud 相關(guān)的文檔。 在過(guò)程中發(fā)現(xiàn) iCloud 的文檔分布非常多, 從設(shè)計(jì)規(guī)范,到基于文檔的編程規(guī)范,等等。散步在很多個(gè)主題文檔中。所以只有把他們?nèi)既趨R起來(lái)才能慢慢的理解這套 API 背后的機(jī)制。
所以呢,這里我把我看到認(rèn)為重要的地方做了一個(gè)梳理。也希望能夠幫助大家少走彎路,抓住重點(diǎn)。