[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 

@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下就有了这个文件:

[iOS]文档操作之UIDocument_第1张图片
保存字符串

读取字符串

读取操作, 只需要将保存的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 

@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);
    }];

运行后就会发现, 本地已经保存一张照片, 和一个文本:

[iOS]文档操作之UIDocument_第2张图片
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)