[iOS]文檔操作之UIDocument

對于文檔的操作, 我們經常使用的是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

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.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 創建自定義文檔對象 基于文檔的應用程序必須具有代表和管理文檔數據的UIDocument子類的實例。本章討論了覆蓋大...
    nicedayCoco閱讀 1,472評論 0 3
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,217評論 30 472
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • UIKit框架為管理多個文檔的應用程序提供支持,每個文檔包含存儲在應用程序沙箱或iCloud中的文件中的唯一數據集...
    nicedayCoco閱讀 4,058評論 0 1
  • 寫了一晚上的文字,都刪了。 發現我的中心思想就是每個人的生活態度,方式都不一樣,如果和你不一樣請你別隨意的評價他人...
    想家想媽媽閱讀 199評論 0 0