[iOS]文档操作之UIDocument

作者: 流火绯瞳 | 来源:发表于2016-12-06 10:14 被阅读690次

    对于文档的操作, 我们经常使用的是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.

    相关文章

      网友评论

        本文标题:[iOS]文档操作之UIDocument

        本文链接:https://www.haomeiwen.com/subject/hkycmttx.html