(译)如何自定义UIDocument的子类
本文节译自Apple iOS开发文档《Document-Based App Programming Guide for iOS》的Creating a Custom Document Object一节
一个文档应用(document-based application)必须使用UIDocument
子类的实例对象来管理文档数据。本节将讨论大多数情况必须复写的方法,并对其他可复写的方法给出建议。对于必须复写的loadFromContents:ofType:error:
和contentsForType:error:
方法,本节将给出两个例子来解释如何使用NSData
和NSFileWrapper
来读写文档数据。本文最后的“将文档数据存储在文件包中“部分将会对后者进一步作出解释。
你还可以复写本文并未涉及的UIDocument
类的其它方法,以实现更多有关文档读取的功能。但这些方法的复写有更复杂的要求,应当尽可能避免。详情可查阅文档UIDocument Class Reference。
声明子类的接口
在Xcode中添加新的Objective-C类,并赋予合适的类名称(建议保留 Document 字眼)。在子类接口文件中,添加新的属性以保留文档数据。
在下例代码一中,文档数据是纯文本,因此只需要一个NSString
属性就足够了(写入文档的文本将转化为NSData
格式)。
代码一 子类声明(NSData)
@interface MyDocument : UIDocument {
}
@property(nonatomic, strong) NSString *documentText;
@end
下例代码二展示了另一个使用NSFileWrappr
对象描述数据类型的app(本节代码样例均基于这两种app)。除了NSFileWrappr
对象属性外,接口文件还添加了文本与图像的属性来描述文档内容。
代码二 子类声明(NSFileWrapper)
@interface ImageNotesDocument : UIDocument
@property (nonatomic, strong) NSString* text;
@property (nonatomic, strong) UIImage* image;
@property (nonatomic, strong) NSFileWrapper *fileWrapper;
@property (nonatomic, weak) id <ImageNotesDocumentDelegate> delegate;
@end
@protocol ImageNotesDocumentDelegate <NSObject>
-(void)noteDocumentContentsUpdated:(ImageNotesDocument*)noteDocument;
@end
代码二还展示了其所使用的代理与协议。文档子类的实例对象所属的视图控制器将作为实例对象的代理,从而在文档发生改变时,得到noteDocumentContentsUpdated: messages
方法的通知。代码四展示了noteDocumentContentsUpdated: messages
方法的使用细节。
加载文档数据
当app依用户需求打开文档时,UIDocument
会发送loadFromContents:ofType:error:
方法读取文档内容,并将内容存储在一个实例对象中。这个实例对象既可以是NSData
类,也可以是NSFileWrapper
类。当复写这个方法时,你应当初始化文档对象的内部数据结构,并将传入的实例对象(the passed-in object)的内容写入其中。
代码三将传入的NSData
对象内容转换为字符串,并赋给了documentText
属性。此外,当文档内容发生变动时,文档对象的代理(也就是其所属的视图控制器)还会得到通知。这是因为loadFromContents:ofType:error:
方法除了会在打开文档时调用外,还会随着iCloud端发生变动时调用(revertToContentsOfURL:completionHandler:
方法)。
代码三 加载文档内容(NSData)
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError {
if ([contents length] > 0) {
self.documentText = [[NSString alloc] initWithData:(NSData *)contents encoding:NSUTF8StringEncoding];
} else {
self.documentText = @"";
}
if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) {
[_delegate noteDocumentContentsUpdated:self];
}
return YES;
}
如果app需要打开多种文档类型,可以设置typeName
参数;不同的文档类型可能需要设置不同的打开方式。如果app在加载文档对象时,遇到错误,这个方法会返回NO
。当然,你也可以设置返回一个NSError
对象来描述所遇到的错误。
代码四 加载文档内容(NSFileWrapper)
-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError {
self.fileWrapper = (NSFileWrapper *)contents;
if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) {
[_delegate noteDocumentContentsUpdated:self];
}
return YES;
}
上例代码四并没有从NSFileWrapper
中提取文本和图像内容,并赋给其对应属性。这些都在text
和image
属性的读取方法中自行完成(lazily done)。
获得文档数据快照
当文档关闭或自动保存时,UIDocument
会向文档对象发送contentsForType:error:
消息。你必须复写这个方法,将文档数据的快照(Snapshot)返回给UIDocument
,然后写入文档文件中。代码五展示如何获得NSData
类型的快照。
代码五 返回数据快照(NSData)
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError {
if (!self.documentText) {
self.documentText = @"";
}
NSData *docData = [self.documentText dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
return docData;
}
如果在创建NSData
对象之前documentText
属性没有赋予任何字符串,该属性将会赋予一个空字符串。
代码六展示如何获得NSFileWrapper
类型的快照。通常,总体NSFileWrapper
对象如果不存在,会由代码自动创建;其内含的文件对象如果不存在,会由代码根据text
和image
属性创建。然后代码会将文档数据的快照(Snapshot)返回给UIDocument
,然后写入文档文件包(file package)中。
文档文件包(file package)会在下一部分得到详细解释。
代码六 返回数据快照(NSFileWrapper)
-(id)contentsForType:(NSString *)typeName error:(NSError **)outError {
if (self.fileWrapper == nil) {
self.fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
}
NSDictionary *fileWrappers = [self.fileWrapper fileWrappers];
if (([fileWrappers objectForKey:TextFileName] == nil) && (self.text != nil)) {
NSData *textData = [self.text dataUsingEncoding:TextFileEncoding];
NSFileWrapper *textFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:textData];
[textFileWrapper setPreferredFilename:TextFileName];
[self.fileWrapper addFileWrapper:textFileWrapper];
}
if (([fileWrappers objectForKey:ImageFileName] == nil) && (self.image != nil)) {
@autoreleasepool {
NSData *imageData = UIImagePNGRepresentation(self.image);
NSFileWrapper *imageFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:imageData];
[imageFileWrapper setPreferredFilename:ImageFileName];
[self.fileWrapper addFileWrapper:imageFileWrapper];
}
}
return self.fileWrapper;
}
将文档数据存储在文件包中
文件包内部存在一定的结构,其反映在NSFileWrapper
类方法中。NSFileWrapper
对象是文件系统节点的运行时代表(a runtime representation of a file-system node)。这个节点可以是目录,普通文档,也可以是符号链接。操作系统将文件系统节点视为一个单独、透明的整体,类似于bundle
概念。
你可以使用代码手动创建一级目录,并向其添加普通文件和子目录,这些都是NSFileWrapper
对象。一级目录当中的NSFileWrapper
对象具有PreferredFilename
属性相互关联。现在,我们会过头来看看代码六中的部分代码:其所创建的文件包内有两个文件——文本文件和图片文件。
if (self.fileWrapper == nil) {
self.fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
}
NSDictionary *fileWrappers = [self.fileWrapper fileWrappers];
if (([fileWrappers objectForKey:TextFileName] == nil) && (self.text != nil)) {
NSData *textData = [self.text dataUsingEncoding:TextFileEncoding];
NSFileWrapper *textFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:textData];
[textFileWrapper setPreferredFilename:TextFileName];
[self.fileWrapper addFileWrapper:textFileWrapper];
}
这段代码会自动创建一级目录,根据text
和image
属性创建文本文件和图片文件,并赋予合适的名字,然后将其添加到一级目录的NSFileWrapper
对象中。
更多NSFileWrapper
类的信息可以查阅NSFileWrapper Class Reference。
有关文档文件包所需的Info.plist
属性可以看Exporting the Document UTI
复写其他方法
你可能会想复写的其他下列UIDocument
类的方法:
-
disableEditing
与enableEditing
。当文档接受来自iCloud端的更新、撤销修改或遇到其他情况导致用户修改文档并不安全时,UIDocument
类会调用前者。你可以复写此方法来避免此时段的文档修改操作。当上述情况解除时,UIDocument
类会调用后者。如果你不愿意复写这两个方法,你还可以利用通知中心观察文档状态的改变。如果文档状态是
UIDocumentStateEditingDisabled
,你应当避免修改操作直到文档状态发生变化。更多有关此话题的信息,可以查阅Monitoring Document-State Changes and Handling Errors。 -
savingFileType
这一方法默认返回fileType
属性的值。如果当前文档需要存储为不同的文档类型,你应该复写此方法来替换文档类型UTI(file-type UTI)。例如Mac OS X系统中,RTF文件中加入图片时会存储为RTFD文件包类型。