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閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,605評論 25 708
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,184評論 30 470
  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,120評論 1 32
  • 自認為我有好多面。 有時候覺得我很好,很努力,很樂于助人,很善良,很… 有時候又覺得自己很陰暗,總把別人想的那么壞...
    a1d2343b2d92閱讀 237評論 0 0
  • 今天在項目中用drawRect畫了一個三角型用于標示section是否折疊,如下圖 當折疊的時候會有一個rotat...
    杰米閱讀 2,209評論 0 0