對于文檔的操作, 我們經常使用的是NSFileManager, 其相關的API使用簡單, 操作方便. 但是還有另外一個操作文件檔的類: UIDocument, 他不但能方便的操作大量的文檔, 而且還能解決異步問題,例如: 在我們使用iCloud進行同步的時候, 不僅僅我們的APP在操作這些內容, 還有iCloud Daemon也有可能在操作這些文檔, 這種多個線程共同操作一個資源的時候, 就需要保證在同一時刻只有一個進程會操作這個資源, 而不是兩個線程共同操作, 這就需要一個同步機制, 這樣 NSFileManager這樣的API就無法保證多個線程之間的這種安全訪問的,而這些, 蘋果對UIDocument底層的封裝都為我們解決了這些問題, 在使用時, 我們不用再關心這些, 而把精力放在文檔處理上就行了.
下面就來看看怎么使用UIDocument.
本文只涉及到以下API的使用:
// 實例化UIDocument對象
- (instancetype)initWithFileURL:(NSURL *)url
// 保存數據, 此方法調用后, 系統或自動調用contentsForType方法返回需要保存數據
// url: 地址 ;
// saveOperation: 枚舉UIDocumentSaveForCreating(新建),UIDocumentSaveForOverwriting(覆蓋原有);
// completionHandler: 保存結果回調
- (void)saveToURL:(NSURL *)url forSaveOperation:(UIDocumentSaveOperation)saveOperation completionHandler:(void (^ __nullable)(BOOL success))completionHandler
// 讀取文檔, 當讀取完畢后, 它會調用loadFromContents方法,
// 在loadFromContents方法中獲取我們要讀取的數據
- (void)openWithCompletionHandler:(void (^ __nullable)(BOOL success))completionHandler
// 調用openWithCompletionHandler, 文檔使用結束后, 要調用此方法來關閉文檔
// 還會為我們自動處理保存以及資源的釋放
- (void)closeWithCompletionHandler:(void (^ __nullable)(BOOL success))completionHandler
需要注意的是: 這里的block回調全部都是異步進行的, 所以不要在調用這些方法后, 就去使用或編輯文件.
定義UIDocument子類
UIDocument是一個抽象類, 我們不能直接使用他, 而應該使用他的子類, 首先我們定義一個類LZDocument, 繼承自UIDocument:
#import <UIKit/UIKit.h>
@interface LZDocument : UIDocument
@end
然后, 實現他的兩個方法, 這兩個方法是必須實現的, 因為我們文件的讀取都依賴于這兩個方法:
- (nullable id)contentsForType:(NSString *)typeName error:(NSError **)outError
- (BOOL)loadFromContents:(id)contents ofType:(nullable NSString *)typeName error:(NSError **)outError
contentsForType方法, 主要是在保存文件的時候使用的, 這里我們需要實現一些邏輯, 把我們需要保存的文檔, 轉換為NSData, 或者NSFileWrapper對象, 然后作為返回值返回, UIDocument會幫我們保存到指定的地址;
loadFromContents方法, 我們需要完善解析出所存數據的邏輯;
以后的所有操作都是使用這個我們自定義的類LZDocument;
獲取本地文檔的URL
使用以下方法, 來獲取一個本地沙盒的URL地址:
// 本地的文件路徑生成URL
+ (NSURL *)urlForFile:(NSString *)fileName {
// 獲取Documents目錄
NSURL *fileUrl = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
// 拼接文件名稱
NSURL *url = [fileUrl URLByAppendingPathComponent:fileName];
NSLog(@"%@", url);
return url;
}
這里為方便使用, 我將他封裝為一個實例方法;
保存字符串
字符串的保存, 一般是處理為NSData, 然后進行返回:
首先, 給LZDocument設置一個字符串類型屬性:
@property (nonatomic, copy) NSString *text;
然后在contentsForType ,添加相應邏輯
- (id)contentsForType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError {
NSLog(@"typeName == %@", typeName);
if (self.text.length <= 0) {
self.text = @"";
}
NSData *data = [self.text dataUsingEncoding:NSUTF8StringEncoding];
return data;
}
然后實例如下:
NSURL *url = [LZDocument urlForFile:@"data.txt"];
// 根據URL創建LZDocument實例
LZDocument *doc = [[LZDocument alloc]initWithFileURL:url];
doc.text = @"這是一串需要保存的字符串";
// 第二個參數
//UIDocumentSaveForCreating, 新建文件
//UIDocumentSaveForOverwriting. 覆蓋原有的文件
[doc saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
NSLog(@"%d",success);
}];
完成有, Documents下就有了這個文件:
讀取字符串
讀取操作, 只需要將保存的NSData 轉換為字符串即可, 在loadFromContents 添加如下邏輯:
// 獲取已保存德爾數據
// 用于 UIDocument 成功打開文件后,我們將數據解析成我們需要的文件內容,然后再保存起來
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError {
self.text = [[NSString alloc]initWithData:contents encoding:NSUTF8StringEncoding];
return YES;
}
然后調用openWithCompletionHandler即可:
// 打開文件
// 當讀取完畢后, 它會調用loadFromContents方法,
// 在loadFromContents方法中獲取我們要讀取的數據
[doc openWithCompletionHandler:^(BOOL success) {
if (success) {
NSLog(@"打開成功");
} else {
NSLog(@"打開失敗");
}
}];
NSLog(@"讀取的數據為: %@",doc.text);
其實不僅僅是字符串可以處理為NSData對象進行保存, 像圖片/文件也可以處理為NSData進行保存;
使用NSFileWrapper
NSFileWrapper存儲在本地的體現是目錄, 外層的NSFileWrapper對象就是父目錄, 里面的NSFileWrapper 就是文件;
下面將LZDocument添加如下屬性:
#import <UIKit/UIKit.h>
@interface LZDocument : UIDocument
@property (nonatomic, strong) UIImage *img;
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) NSFileWrapper *wrapper;
+ (NSURL *)urlForFile:(NSString *)fileName;
@end
保存NSFileWrapper
完善contentsForType內的相關邏輯:
- (id)contentsForType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError {
NSLog(@"typeName == %@", typeName);
if (self.wrapper == nil) {
self.wrapper =[[NSFileWrapper alloc]initDirectoryWithFileWrappers:@{}];
}
NSDictionary *wrappers = [self.wrapper fileWrappers];
if ([wrappers objectForKey:textFileName] == nil && self.text != nil) {
NSData *textData = [self.text dataUsingEncoding:NSUTF8StringEncoding];
NSFileWrapper *textWrap = [[NSFileWrapper alloc]initRegularFileWithContents:textData];
[textWrap setPreferredFilename:textFileName];
[self.wrapper addFileWrapper:textWrap];
}
if ([wrappers objectForKey:imageFileName] == nil && self.img != nil) {
NSData *imgData = UIImageJPEGRepresentation(self.img, 1.0);
NSFileWrapper *imgWrap = [[NSFileWrapper alloc]initRegularFileWithContents:imgData];
[imgWrap setPreferredFilename:imageFileName];
[self.wrapper addFileWrapper:imgWrap];
}
return self.wrapper;
}
這里的文件名稱, 我是定義了兩個字符串:
static NSString *textFileName = @"textfile.txt";
static NSString *imageFileName = @"imageFile.png";
然后, 實例代碼如下:
UIImage *img = [UIImage imageNamed:@"5fdf8db1cb134954979ddf0d564e9258d0094ad3.jpg"];
NSURL *url = [LZDocument urlForFile:@"wrapper"];
// 根據URL創建LZDocument實例
LZDocument *doc = [[LZDocument alloc]initWithFileURL:url];
doc.text = @"這是一串需要保存的字符串";
doc.img = img;
[doc saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
NSLog(@"%d",success);
}];
運行后就會發現, 本地已經保存一張照片, 和一個文本:
獲取NSFileWrapper
從NSFileWrapper中獲取保存的數據:
當我們調用openWithCompletionHandler打開文件的時候, 系統會自動調用loadFromContents, 這里我們解析出保存的數據;
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError {
// 這個NSFileWrapper對象是a parent
self.wrapper = (NSFileWrapper*)contents;
NSDictionary *fileWrappers = self.wrapper.fileWrappers;
// 獲取child fileWrapper 這里才能獲取到我們保存的內容
NSFileWrapper *textWrap = [fileWrappers objectForKey:textFileName];
NSFileWrapper *imgWrap = [fileWrappers objectForKey:imageFileName];
// 獲取保存的內容
self.text = [[NSString alloc]initWithData:textWrap.regularFileContents encoding:NSUTF8StringEncoding];
self.img = [UIImage imageWithData:imgWrap.regularFileContents];
return YES;
}
這個方法的回調參數contents, 其實就是一個父級的fileWrapper對象, 其中包含的child對象才是真正包含我們所需要的數據的, 所以這里進行了逐級的數據解析, 主要最后在獲取數據的時候regularFileContents屬性:
/* This method throws an exception when [receiver isRegularFile]==NO. */
/* Return the receiver's contents. This may return nil if the receiver is the result of reading a parent from the file system (use NSFileWrapperReadingImmediately if appropriate to prevent that).
*/
@property (nullable, readonly, copy) NSData *regularFileContents;
如果當前的fileWrapper對象是父級的, 這個值是nil, 其regularFile屬性為NO, 所以這里可以使用這個屬性來判斷一下, 是否包含regularFileContents:
if (textWrap.regularFile) {
self.text = [[NSString alloc]initWithData:textWrap.regularFileContents encoding:NSUTF8StringEncoding];
}
這樣, 就取出了, 我們所保存的數據;
最后, 需要注意的是, 前面提到的方法:
- saveToURL
- openWithCompletionHandler
- closeWithCompletionHandler
都是異步進行的.
補充
因為, 上面的操作都是異步進行的, 所以在我們獲取數據的時候不好把握時機, 這時, 我們可以使用代理來獲取.
另外, 在操作本地(沙盒)文檔時, 我們很少會選擇UIDocument, 更多的使用的場合是關于iCloud文檔的操作.
最后附上一個demo, 只是完成第二種方式的操作: github地址
以及一個實際的應用, iCloud云存儲中的使用: LZiCloudDemo
使用中遇到的問題
設備間同步數據錯誤
在使用NSFileWrapper保存數據的時候, 如果進行設備間數據共享, 存取數據會有些差異, 在contentsForType:error:方法中進行保存的操作:
[textWrap setPreferredFilename:textFileName];
和在loadFromContents:(id)contents ofType:error:方法中獲取子NSFileWrapper實例的時候:
// 獲取child fileWrapper 這里才能獲取到我們保存的內容
NSFileWrapper *textWrap = [fileWrappers objectForKey:textFileName];
這里的key值是對應的, 這樣在同一設備進行iCloud同步是沒有問題的, 但是在設備之間, 使用同一個iCloud賬號進行數據共享的時候, 這個key值會發生變化.
例如: 設置key值為: "myKey",在一個設備上備份數據到iCloud上, 然后使用一個新的設備, 從iCloud備份數據至新的設備, 這個key值會變為: ".myKey.icloud"; 如果, 在新的設備上,進行了一次保存至iCloud操作后, 這個key值就又是原來的值: myKey,所以這個需要特殊處理一下, 在新的設備進行首次同步操作時, 特殊處理一下這個key.