(译)如何自定义UIDocument的子类

本文节译自Apple iOS开发文档《Document-Based App Programming Guide for iOS》的Creating a Custom Document Object一节

一个文档应用(document-based application)必须使用UIDocument子类的实例对象来管理文档数据。本节将讨论大多数情况必须复写的方法,并对其他可复写的方法给出建议。对于必须复写的loadFromContents:ofType:error:contentsForType:error:方法,本节将给出两个例子来解释如何使用NSDataNSFileWrapper来读写文档数据。本文最后的“将文档数据存储在文件包中“部分将会对后者进一步作出解释。

你还可以复写本文并未涉及的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  delegate;
@end
 
@protocol ImageNotesDocumentDelegate 
- (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中提取文本和图像内容,并赋给其对应属性。这些都在textimage属性的读取方法中自行完成(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对象如果不存在,会由代码自动创建;其内含的文件对象如果不存在,会由代码根据textimage属性创建。然后代码会将文档数据的快照(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概念。

(译)如何自定义UIDocument的子类_第1张图片
文件包的结构 - 来自官方文档

你可以使用代码手动创建一级目录,并向其添加普通文件和子目录,这些都是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];
    }

这段代码会自动创建一级目录,根据textimage属性创建文本文件和图片文件,并赋予合适的名字,然后将其添加到一级目录的NSFileWrapper对象中。

更多NSFileWrapper类的信息可以查阅NSFileWrapper Class Reference。

有关文档文件包所需的Info.plist属性可以看Exporting the Document UTI

复写其他方法

你可能会想复写的其他下列UIDocument类的方法:

  • disableEditingenableEditing。当文档接受来自iCloud端的更新、撤销修改或遇到其他情况导致用户修改文档并不安全时,UIDocument类会调用前者。你可以复写此方法来避免此时段的文档修改操作。当上述情况解除时,UIDocument类会调用后者。

    如果你不愿意复写这两个方法,你还可以利用通知中心观察文档状态的改变。如果文档状态是UIDocumentStateEditingDisabled,你应当避免修改操作直到文档状态发生变化。更多有关此话题的信息,可以查阅Monitoring Document-State Changes and Handling Errors。

  • savingFileType这一方法默认返回fileType属性的值。如果当前文档需要存储为不同的文档类型,你应该复写此方法来替换文档类型UTI(file-type UTI)。例如Mac OS X系统中,RTF文件中加入图片时会存储为RTFD文件包类型。

你可能感兴趣的:((译)如何自定义UIDocument的子类)