[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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容

  • 创建自定义文档对象 基于文档的应用程序必须具有代表和管理文档数据的UIDocument子类的实例。本章讨论了覆盖大...
    nicedayCoco阅读 1,430评论 0 3
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,139评论 30 470
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,652评论 18 139
  • UIKit框架为管理多个文档的应用程序提供支持,每个文档包含存储在应用程序沙箱或iCloud中的文件中的唯一数据集...
    nicedayCoco阅读 3,713评论 0 1
  • 写了一晚上的文字,都删了。 发现我的中心思想就是每个人的生活态度,方式都不一样,如果和你不一样请你别随意的评价他人...
    想家想妈妈阅读 188评论 0 0