持久化

沙箱


iOS app中沙箱的概念不用多说了。在app沙箱中,包含了一些标准的目录。例如,Documents 目录:

NSString* docs = [NSSearchPathForDirectoriesInDomains(
    NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

如果你想拿到这个Documents目录的URL,可以:

NSFileManager* fm = [NSFileManager new];
NSError* err = nil;
NSURL* docsurl =
    [fm URLForDirectory:NSDocumentDirectory
               inDomain:NSUserDomainMask appropriateForURL:nil
                 create:YES error:&err];
// 错误检查

但是,我们究竟要把数据放哪呢?这是一个问题,Documents目录是一个好的选择,但是如果你的app 支持文件分享(后面会讲),用户可以通过iTunes看到你的Documents目录和修改这个目录下的文件,因此你可能不想把一些不希望用户看到或者修改的文件翻到这个目录下。

个人来说,我会喜欢使用Application Support 目录。在Mac中,这个目录是多个应用程序共享的,每个应用程序必须在里面各自建立一个子目录来管理,但是在iOS中,每个app的沙箱里都有它私有的Application Support目录,你可以这样获得这个目录:

NSURL* suppurl =
   [fm URLForDirectory:NSApplicationSupportDirectory
              inDomain:NSUserDomainMask appropriateForURL:nil
                create:YES error:&err];

在苹果的File System Programming Guide 文档的“File System Basics” 章节中有一篇文章“Where You Should Put Your App’s Files” ,里面讲解了当用户同步或者备份设备时,对不同的文件存储路径的影响。

文件分享


如果你的app支持文件分享,那么用户就可以通过iTunes访问和修改你的Documents目录。

为了支持文件分享,需要在 Info.plist 文件 中设置 key “Application supports iTunes file sharing” 的值 为 UIFileSharingEnabled。

当用户修改了你app的Documents目录,你的app不会收到任何的通知。所以完全由你来决定怎么去响应这个变化了。

文件类型


您的应用程序可以声明自己可以打开某种类型的文件。这样一来,如果其他应用程序获得这种类型的文件,也可以提出交出文件给您的应用程序来打开。例如,用户可能从Safari浏览器下载了或者从邮件app中收到了一个文件,现在我们需要一种方法来从这些app中拿到这个文件。

为了让系统知道你的app可以处理或者打开某种类型的文件,你需要在 Info.plist文件中配置 “Document types” 键(CFBundleDocumentTypes)。 这是一个数组,每个支持的文件类型使用key “Document Content Type UTIs” (LSItemContentTypes),“Document Type Name”(CFBundleTypeName),CFBundleTypeIconFiles和LSHandlerRank来表示。

例如,下面我添加一个PDF文件类型的支持:

现在,当我们在Safari或者邮件app中,点击这个文件时,可能会像下面这样提示:

假设用户真的点击了我们的app图标来打开这个文档。我们需要实现application:handleOpenURL: 方法。当这个方法被调用时,这个url表示该文件,同时系统会帮我们把这个文件拷贝到我们app的沙箱下面的Inbox 目录(这个目录是创建在Documents目录里面的):

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    [self.viewController displayPDF:url];
    return YES;
}

然后在我的视图控制器中,包含下面的代码:

- (void) displayPDF: (NSURL*) url {
    NSURLRequest* req = [NSURLRequest requestWithURL:url];
    [self.wv loadRequest:req];
}

在现实中,这个处理过程可能会更负责些,你需要在application:handleOpenURL: 方法中检查这个URL是否真的是一个PDF文件,如果不是,则要返回NO。同时,当这个方法被调用时,你的app可能正在另外的视图控制器中运行,这个方法可能随时被调用,你必须准备丢弃正在做的东西,显示传进来的这个文档。

如果我们的app是从头开始进入,application:didFinishLaunchingWithOptions:会如往常一样被调用。这个options参数将会包含这个UIApplicationLaunchOptionsURLKey,如果你喜欢,你可以直接打开指定的文档。但是通常的做法是忽略这个key,让app正常启动,然后我们可以在 application:handleOpenURL: 方法中对要打开的文件处理。

你也可以实现 application:openURL:sourceApplication:annotation:来获取更多的关于传进来的URL的信息,而且这个方法会优先于 application:handleOpenURL: 方法被调用。

假设你的app有一个PDF文档在Documents目录下,我们有一个URL指向这个Documents目录,你希望让用户选择用其它的app打开:

self.dic =
    [UIDocumentInteractionController interactionControllerWithURL:url];
BOOL y =
    [self.dic presentOpenInMenuFromRect:[sender bounds]
                                 inView:sender animated:YES];

在iOS 6中,显示的是一个 activity 视图(action sheet),跟上面的选择视图一样。实际上还有两种activity视图可用:

presentOpenInMenuFromRect:inView:animated:

presentOpenInMenuFromBarButtonItem:animated:

显示一个activity视图,列举了所有能够打开这个文档的app。

presentOptionsMenuFromRect:inView:animated:

presentOptionsMenuFromBarButtonItem:animated:

显示一个activity视图,除了列举所有能够打开这个文档的app外,还有一些而外的动作,例如 Print,Copy和 Mail。

UIDocumentInteractionController还可以显示一个文档的预览图。如果这样,你必须提供UIDocumentInteractionController一个委托(UIDocumentInteractionControllerDelegate),然后在这个委托对象中,实现 documentInteractionControllerViewControllerForPreview: 方法,返回将要包括这个预览视图控制器的视图控制器,下面请求预览:

self.dic =
    [UIDocumentInteractionController interactionControllerWithURL:url];
self.dic.delegate = self;
[self.dic presentPreviewAnimated:YES];

在委托中,我们返回自身这个视图控制器:

- (UIViewController *) documentInteractionControllerViewControllerForPreview:
        (UIDocumentInteractionController *) controller {
    return self;
}

如果返回的这个视图控制器是一个UINavigationController,这个预览视图控制器将会 push 在它上面。在我们例子中,返回的视图控制器不是一个UINavigationController,所以这个预览视图控制器是有一个 Done 按钮的 展示型视图控制器。同时这个展示视图控制器也会有一个 Action 按钮,让用户可以选择用什么app来打开这个文档。

委托方法允许你跟踪这个UIDocumentInteractionController视图控制器发生了什么:

  • documentInteractionControllerDidDismissOptionsMenu:

  • documentInteractionControllerDidDismissOpenInMenu:

  • documentInteractionControllerDidEndPreview:

  • documentInteractionController:didEndSendingToApplication:

预览的功能实际上是由Quick Look framework 提供的。你可以跳过UIDocumentInteractionController,直接使用QLPreviewController来展示预览。QLPreviewController的一个很好的功能是你可以提供多个文档进行预览。用户可以在切换不同的文档。

下面的例子中,我在Documents目录下有多个PDF文档,通为他们提供一个预览:

// obtain URLs of PDFs as an array
NSFileManager* fm = [NSFileManager new];
NSURL* docsurl =
    [fm URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask
        appropriateForURL:nil create:NO error:nil];
NSDirectoryEnumerator* dir =
    [fm enumeratorAtURL:[docsurl URLByAppendingPathComponent:@"Inbox"]
        includingPropertiesForKeys:nil options:0 errorHandler:nil];
if (!dir)
    return; // proper error-checking omitted
NSMutableArray* marr = [NSMutableArray array];
for (NSURL* f in dir) {
    [dir skipDescendants];
    if ([[f pathExtension] isEqualToString: @"pdf"])
        [marr addObject: f];
}
self.pdfs = marr; // retain policy
if (![self.pdfs count])
    return;
// show preview interface
QLPreviewController* preview = [QLPreviewController new];
preview.dataSource = self;
[self presentViewController:preview animated:YES completion:nil];

实现以下数据源方法:

- (NSInteger) numberOfPreviewItemsInPreviewController:
        (QLPreviewController *) controller {
    return [self.pdfs count];
}
- (id <QLPreviewItem>) previewController: (QLPreviewController *) controller
                      previewItemAtIndex: (NSInteger) index {
    return self.pdfs[index];
}

文件结构


如果你的app需要打开和保存一种自身特有类型的文档,你可能会用到文档结构相关的技术。 这个文档结构由一个UIDocument类帮我们解决了一些很烦人的操作。例如我们加载或写数据时可能需要消耗时间。另外UIDocument也提供了自动保存的功能,当数据发生改变时。同时,UIDocument是把你的文档放到iCloud上的一个门,以使你app的文档自动同步到不同的设备上。

首先我们要子类话UIDocument,重写两个方法:

loadFromContents:ofType:error:

当要从磁盘上打开一个文档时被调用。你会希望把这个文档内容转换到一个模型对象上,以便你的app使用,保存这个模型对象后,返回YES(如果期间出现了错误,你将要设置这个error 指针对应的数据,然后返回NO)

contentsForType:error:

当要保存一个文档到磁盘上时被调用。你会希望把模型对象的数据转换到NSData实例上(或者,如果你的目录是一个包,可以转换到NSFileWrapper),然后返回这个实例。(如果期间出现了错误,你将要设置这个error指针对应的数据,返回nil)

大多数情况下,你的视图控制器会用到这个子类UIDocument对象里面的数据,你可以提供一个委托,让这个UIDocument子类通过委托设置视图控制器的相关数据属性。

要初始化一个UIDocument,可以调用它的初始化方法 initWithFileURL: 。 这个方法会设置UIDocument的 fileURL属性,然后把这个文件与UIDocument对应起来。

那么我们的UIDocument怎么知道某个时刻是打开文档还是保存文档呢? 主要有下面几个时刻:

创建一个新的文档

表示fileURL: 指向一个不存在的文件。这时在初始化后,立刻调用方法 saveToURL:forSaveOperation:completionHandler:,第二个参数是UIDocumentSaveForCreating。这样就会导致contentsForType:error: 方法被调用,最后这个空的文档会被保存到磁盘上。

打开一个已经存在的文档

给UIDocument 实例发送 openWithCompletionHandler:消息。这样就会导致 loadFromContents:ofType:error:方法被调用。

保存一个已经存在的文档

有两种方法保存一个文档。

  • 自动保存

    当你觉得你的文档数据改变了,你可以不断调用 updateChangeCount: 方法。UIDocument会注意到这个状态,将会保存文档。在保存过程中,会调用contentsForType:error:方法。

  • 手动保存

    调用 saveToURL:forSaveOperation:completionHandler: 方法,如果文档不是第一次创建的,第二个参数是UIDocumentSaveForOverwriting。 如果你确定你已经完成了文档的操作,你还可以调用closeWithCompletionHandler: 方法。

下面我们举个小例子:

我们首先定义一个UTI 在我们的 Info.plist 文件中, 把一个文件类型(com.neuburg.pplgrp)与 一个文件后缀名(@"pplgrp")联系起来,如上图所示。

这个文档会包括多个Persons。我们子类化一个 UIDocument (叫做PeopleDocument),有一个people属性,可以实现如下:

-(id)initWithFileURL:(NSURL *)url {
    self = [super initWithFileURL:url];
    if (self) {
        self->_people = [NSMutableArray array];
}
    return self;
}
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName
                   error:(NSError **)outError {
    NSArray* arr = [NSKeyedUnarchiver unarchiveObjectWithData:contents];
    self.people = [NSMutableArray arrayWithArray:arr];
    return YES;
}
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError {
    NSData* data = [NSKeyedArchiver archivedDataWithRootObject:self.people];
    return data;
}

下面是如何使用这个PeopleDocument:

NSFileManager* fm = [NSFileManager new];
self.doc = [[PeopleDocument alloc] initWithFileURL:self.fileURL];
void (^listPeople) (BOOL) = ^(BOOL success) {
    if (success) {
        self.people = self.doc.people;
        [self.tableView reloadData];
    } 
};
if (![fm fileExistsAtPath:[self.fileURL path]])
    [self.doc saveToURL:doc.fileURL
       forSaveOperation:UIDocumentSaveForCreating
      completionHandler:listPeople];
else
    [self.doc openWithCompletionHandler:listPeople];

当用户修改了这个文档的数据后,我们可以告诉UIDocument自动保存:

[self.doc updateChangeCount:UIDocumentChangeDone];

当app进入后台,或者当这个视图正在消失时,它会强制PeopleDocument 立刻保存:

- (void) forceSave: (id) n {
    [self.tableView endEditing:YES];
    [self.doc saveToURL:doc.fileURL
        forSaveOperation:UIDocumentSaveForOverwriting
        completionHandler:nil];
}
posted @ 2014-03-21 11:43  剑尖  阅读(282)  评论(0编辑  收藏  举报