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