Document-Based App Programming Guide for iOS (二)

创建自定义文档对象

基于文档的应用程序必须具有代表和管理文档数据的UIDocument子类的实例。本章讨论了覆盖大多数应用程序所需的方法,并提供了覆盖其他方法的建议。对于核心重写点,loadFromContents:ofType:error:和contentsForType:error:methods-examples被给定为NSData和NSFileWrapper作为从文件读取和写入文档数据的类型。将文档数据存储在文件包中进一步说明了如何对文档数据使用文件包装对象。

除了本章讨论的UIDocument以外,您可以覆盖UIDocument的方法,以便为特定目的读取和写入文档数据,例如逐步写入和读取文档数据。然而,这些更高级的覆盖具有更复杂的要求,如果可能的话应该避免。有关这些覆盖的讨论,请参阅UIDocument类参考。

声明文档类接口

在Xcode中,将新的Objective-C源文件和头文件添加到您的项目中,并将其正确命名(建议:将“文档”作为名称)。在头文件中,将超类更改为UIDocument,并添加属性以保存文档数据。在清单3-1中,文档数据是纯文本,因此NSString属性是保存它所需要的。 (文本将被转换为写入文档文件的NSData对象。)

清单3-1文档子类声明(NSData)

@interface MyDocument : UIDocument {
}
@property(nonatomic, strong) NSString *documentText;
@end

清单3-2说明了另一个使用NSFileWrapper对象作为数据表示类型的应用程序的声明集。 (本章中的代码示例在两个应用程序之间交替)。不仅有一个属性来保存文件包装对象,还有一些属性可以保存表示文件包的文本和图像组件。

清单3-2文档子类声明(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)进行通知。清单3-4显示了发送noteDocumentContentsUpdated:消息的时间和方式。

加载文档数据

当应用程序打开文档(按照用户的请求)时,UIDocument将读取文档文件的内容,并调用loadFromContents:ofType:error:method,传递封装文档数据的对象。该对象可以是NSData对象或NSFileWrapper对象。在覆盖该方法时,从传入对象的内容中初始化文档的内部数据结构(即其模型对象)。

清单3-3中的示例从传入的NSData对象创建一个字符串,并将其分配给documentText属性。它还通过调用协议方法通知其代表(在这种情况下,文档的视图控制器)更新的文档内容。这个委托消息背后的动机是loadFromContents:ofType:error:method不仅是打开文档的结果,也是因为iCloud更新和恢复操作(revertToContentsOfURL:completionHandler :)的结果。

清单3-3加载文档的数据(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;
}

如果您有多个文档类型,请检查typeName参数; 不同的文档类型可能会影响代码如何处理文档数据对象。 如果您的代码遇到阻止其加载文档数据的错误,请返回NO; 可选地,您可以通过引用返回描述错误的NSError对象。

清单3-4中的示例以NSFileWrapper对象的形式处理文档数据。 它只是将此对象分配给其属性。

清单3-4加载文档的数据(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;
}

在此代码中,方法实现不会提取文件包装器的文本和图像组件,并将其分配给其属性。 这在文本和图像属性的getter方法中做得很懒。

提供文件数据快照

当文档关闭或自动保存文档时,UIDocument会将文档对象发送一个contentsForType:error:message。 您必须重写此方法以将文档数据的快照返回到UIDocument,然后将其写入文档文件。 清单3-5给出了以NSData对象的形式返回文档数据快照的示例。

清单3-5返回文档数据的快照(NSData)

- (id)contentsForType:(NSString *)typeName error:(NSError **)outError {
    if (!self.documentText) {
        self.documentText = @"";
    }
    NSData *docData = [self.documentText dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
    return docData;
}

如果documentText属性尚未分配任何字符串值,则在使用它创建NSData对象之前,将为其分配一个空字符串。

清单3-6显示了返回NSFileWrapper对象的相同方法的实现。 基本上,如果顶级(目录)文件包装对象不存在,则代码创建它; 并且如果两个包含(常规文件)文件包装对象不存在,则代码将从文本和图像属性的值创建它们。 然后,它将顶级文件包装器返回到UIDocument,该文件在文件系统中创建一个文件包。 请参阅将文档数据存储在文件包中,以获取有关文件包和文档的更详细说明。

清单3-6返回文档数据的快照(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类的方法中。 文件包装器是文件系统节点的运行时表示,它是目录,常规文件或符号链接。 如图3-1所示,文件包是文件系统节点,通常是目录及其内容,操作系统将其视为单个不透明实体。 它在概念上与捆绑类似。

图3-1文件包的结构

document_file_package_2x.png

您可以通过创建一个顶级目录文件包装器来编程文件包,然后向该容器添加常规文件和子目录,每个文件和子目录由其他NSFileWrapper对象表示。 顶层目录中的文件包装应该具有与它们相关联的首选名称。

考虑到这个简要概述,请再次查看清单3-6中的contentsForType:error:方法中的以下代码行。 在此方法中创建的文件包具有两个组件,一个文本文件和一个图像文件。 (图片文件包装器的创建不会显示在代码段中。)

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属性的字符串内容中创建一个。它给这个文件包装器一个首选文件名,然后将其添加到顶级目录文件包装器。

有关NSFileWrapper的更多信息,请参阅NSFileWrapper类参考;另请参阅导出文档UTI的文档文件包所需的Info.plist属性。

其他方法可以覆盖你可能做的

许多基于文档的应用程序可能想要做的其他一些UIDocument覆盖:

disableEditing ... enableEditing-UIDocument在不安全的情况下调用第一种方法,以便用户更改文档内容,例如当iCloud有更新或还原操作正在进行时。您可以在此期间实施此方法以防止编辑。再次编辑变得安全时,UIDocument调用第二种方法。
注意:作为这些覆盖的替代方法,您可以观察文档状态更改时发布的通知,如果新文档状态为UIDocumentStateEditingDisabled,则会在文档状态再次更改之前防止编辑。有关此主题的更多信息,请参阅监控文档状态更改和处理错误。
savingFileType - 默认情况下,此方法返回fileType属性的值。如果由于任何原因而将当前文档保存在不同的文件类型下,可以覆盖此方法以返回替换文件类型UTI。一个例子(来自Mac OS X)是将图像添加到RTF文件时,应将其保存为RTFD文件包。

管理文件的生命周期

一个文件经历了一个典型的生命周期。基于文档的应用程序负责管理其在该周期中的进展。从以下列表可以看出,大多数这些生命周期事件是由用户发起的:

  • 用户首先创建一个文档。
  • 用户打开现有文档,应用程序将其显示在文档的视图或视图中。
  • 用户编辑文档。
  • 用户可能会要求将文档放在iCloud存储中,或者可以请求从iCloud存储中删除文档。
  • 在编辑,保存或其他操作期间,可能会发生错误或冲突;应用程序应该了解这些错误和冲突,并尝试处理它们或通知用户。
  • 用户关闭选定的文档。
  • 用户删除现有文档。
    以下部分将讨论基于文档的应用程序必须为这些生命周期操作完成的过程。

设置文档文件的首选存储位置

应用程序的所有文档都存储在本地沙箱或iCloud容器目录中。用户不能选择单独的文档存储在iCloud中。

应用程序首次在设备上启动应用程序时,应执行以下操作:

  • 如果iCloud未配置,请告知用户,如果要在其中保存文件,则需要配置iCloud。
  • 如果iCloud已配置但未启用应用程序,请询问用户是否要启用iCloud,换句话说,询问他们是否希望将所有文档保存到iCloud。将响应存储为用户偏好。

基于此首选项,应用程序将文档文件写入本地应用程序沙箱或iCloud容器目录。 (有关详细信息,请参阅将文档移入和移出iCloud Storage。)应用程序应在“设置”应用程序中公开交换机,以使用户能够在本地存储和iCloud存储之间移动文档。

创建新文档

文档对象(即,您的自定义UIDocument子类的实例)必须具有将文档文件定位到本地应用程序沙箱或iCloud容器目录中的文件URL,取决于用户的偏好。另外,一个新的文件可以给一个名字。以下讨论与文件URL,文档名称和创建新文档相关的准则和过程。

文件文件名与文件名称

UIDocument类假定文档的文件名与文档名称(也称为显示名称)之间的对应关系。默认情况下,UIDocument将文件名存储为localizedName属性的值。但是,当应用程序创建新文档时,应用程序不应要求用户提供文档名称或显示名称。

对于您的应用程序,您应该制定一些自动生成新文档的文件名的约定。一些建议是:

  • 为每个文档生成UUID(通用唯一标识符),可选地使用应用程序特定的前缀。
  • 为每个文档生成时间戳(日期和时间),可选地使用应用程序特定的前缀。

使用顺序编号系统,例如:“注1”,“注2”等。
对于文档(显示)名称,如果这是有意义的(例如使用“Notes 1”),则可能最初使用文档文件名。或者,如果文档包含文本,并且用户在文档中输入一些文本,则可以使用第一行(或第一行的某些部分)作为显示名称。您的应用程序可以在用户创建文档之后给用户一些自定义文档名称的方法。

编写文件URL并保存文档文件

您无法创建没有有效的文件URL的文档对象。文件URL有三个部分:文档目录在用户首选文档位置的路径,文档文件名和文档文件的扩展名。您可以通过文档文件名与文档名称中的方法获取表示本地应用程序沙箱中的Documents目录的路径的URL。

清单4-1获取本地沙箱中应用程序的Documents目录的URL

-(NSURL*)localDocumentsDirectoryURL {
    static NSURL *localDocumentsDirectoryURL = nil;
    if (localDocumentsDirectoryURL == nil) {
        NSString *documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains( NSDocumentDirectory,
            NSUserDomainMask, YES ) objectAtIndex:0];
        localDocumentsDirectoryURL = [NSURL fileURLWithPath:documentsDirectoryPath];
    }
    return localDocumentsDirectoryURL;
}

文件扩展名必须是您为文档类型指定的扩展名(请参阅创建和配置项目)。 您可以声明一个全局字符串来表示扩展名。 例如:

static NSString *FileExtension = @"imageNotes”;

文档URL的最后一部分是文件名组件。当文档文件名与文档名称解释时,应用程序最初应根据对应用程序有意义的约定生成文档文件名。该生成的文件名可以用作文档名称,也可以将第一行(或其一部分)用作文档名称。该应用程序可以给用户在创建文档对象后自定义文档名称的选项。

连接基本URL,文档文件名和文件扩展名后,您可以分配一个自定义UIDocument子类的实例,并使用initWithFileURL:方法初始化它,传递构造的文件URL。创建新文档的最后一步是将其保存到首选文档存储位置(即使此时没有内容)。如通过设置文档文件的首选存储位置所示,您可以通过调用文档对象上的saveToURL:forSaveOperation:completionHandler:method来执行此操作。

清单4-2将新文档保存到文件系统

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    if (_createFile) {
        [self.document saveToURL:self.document.fileURL
            forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            if (success)
                _textView.text = self.document.text;
        }];
        _createFile = NO;
    }
    // .....
}

方法调用的save-operation参数应为UIDocumentSaveForCreating。调用的最终参数是完成处理程序:在保存操作结束后调用的块。该块的参数告诉您操作是否成功。如果确实成功,则此代码将文档文本分配给显示文档内容的文本视图的text属性。

注意:如果要将新文档保存到应用程序的iCloud容器目录中,建议首先将其保存在本地,然后调用NSFileManager方法setUbiquitous:itemAtURL:destinationURL:error:将文档文件移动到iCloud存储。 (可以在saveToURL的完成处理程序中进行此调用:forSaveOperation:completionHandler:method。)有关更多信息,请参阅将文档移动到iCloud Storage和iCloud Storage。

打开和关闭文档

打开文件可能乍看起来似乎是一个相当简单的过程。您的应用程序扫描其文档目录中的文件的文件的文件扩展名,并将这些文档提供给用户进行选择。然而,当iCloud存储被考虑在内时,事情会变得更加复杂。应用程序的文档可能位于应用程序沙箱的Documents目录中,也可能位于iCloud容器目录的Documents目录中。

发现申请文件

要获取iCloud存储中应用程序文档的列表,请运行元数据查询。查询是NSMetadataQuery类的一个实例。创建一个NSMetadataQuery对象后,给它一个范围和一个谓词。对于iCloud存储,范围应为NSMetadataQueryUbiquitousDocumentsScope。谓词是一个NSPredicate对象,在这种情况下,限制文件扩展名的搜索。在开始运行查询之前,请注册以观察NSMetadataQueryDidFinishGatheringNotification和NSMetadataQueryDidUpdateNotification通知。接受传递这些通知的方法处理查询的结果。

清单4-3说明了如何设置和运行元数据查询以获取iCloud移动容器中的应用程序文档列表。该方法首先测试用户对文档(documentsInCloud属性)的首选存储位置。如果该位置是移动容器,则它运行元数据查询。如果位置是应用程序沙箱,它会遍历应用程序的Documents目录的内容,以获取所有本地文档文件的名称和位置。

清单4-3获取存储在本地和iCloud存储中的文档的位置

-(void)viewDidLoad {
    [super viewDidLoad];
    // set up Add and Edit navigation items here....

   if (self.documentsInCloud) {
        _query = [[NSMetadataQuery alloc] init];
        [_query setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope, nil]];
        [_query setPredicate:[NSPredicate predicateWithFormat:@"%K LIKE '*.txt'", NSMetadataItemFSNameKey]];
        NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
        [notificationCenter addObserver:self selector:@selector(fileListReceived)
            name:NSMetadataQueryDidFinishGatheringNotification object:nil];
        [notificationCenter addObserver:self selector:@selector(fileListReceived)
            name:NSMetadataQueryDidUpdateNotification object:nil];
        [_query startQuery];
    } else {
        NSArray* localDocuments = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:
            [self.documentsDir path] error:nil];
        for (NSString* document in localDocuments) {
            [_fileList addObject:[[[FileRepresentation alloc] initWithFileName:[document lastPathComponent]
                url:[NSURL fileURLWithPath:[[self.documentsDir path]
                stringByAppendingPathComponent:document]]] autorelease]];
        }
    }
}

在这个例子中,谓词格式是@“%K LIKE'* .txt'”,这意味着返回所有文件名(NSMetadataItemFSNameKey键),扩展名为txt,这是该应用程序文档文件的文件扩展名。

初始查询结束后,如果有后续更新,则再次调用清单4-3(fileListReceived)中指定的通知方法。 清单4-4显示了该方法的实现。 如果查询更新在用户进行选择后到达,则代码还会跟踪当前选择。

清单4-4收集有关iCloud存储中的文档的信息

-(void)fileListReceived {

 NSString* selectedFileName=nil;
    NSInteger newSelectionRow = [self.tableView indexPathForSelectedRow].row;
    if (newSelectionRow != NSNotFound) {
        selectedFileName = [[_fileList objectAtIndex:newSelectionRow] fileName];
    }
    [_fileList removeAllObjects];
    NSArray* queryResults = [_query results];
    for (NSMetadataItem* result in queryResults) {
        NSString* fileName = [result valueForAttribute:NSMetadataItemFSNameKey];
        if (selectedFileName && [selectedFileName isEqualToString:fileName]) {
            newSelectionRow = [_fileList count];
        }
        [_fileList addObject:[[[FileRepresentation alloc] initWithFileName:fileName
            url:[result valueForAttribute:NSMetadataItemURLKey]] autorelease]];
    }
    [self.tableView reloadData];
    if (newSelectionRow != NSNotFound) {
        NSIndexPath* selectionPath = [NSIndexPath indexPathForRow:newSelectionRow inSection:0];
        [self.tableView selectRowAtIndexPath:selectionPath animated:NO scrollPosition:UITableViewScrollPositionNone];
    }
}

示例应用程序现在具有一个定制模型对象的数组(_fileList),该对象封装了每个应用程序文档的名称和文件URL。 (FileRepresentation是这些对象的自定义类。)根视图控制器使用文档名称填充一个简单表视图

注意:您应该将元数据查询仅在您的应用程序处于前台时运行。当应用程序移动到后台时,您应该停止查询。

从iCloud下载文件文件

运行元数据查询以了解应用程序的iCloud文档时,查询结果将是文档文件的占位符项(NSMetadataItem对象)。这些项目包含有关文件的元数据,例如其URL及其修改日期。文档文件不在iCloud容器目录中。

直到发生以下情况之一才会下载文档的实际数据:

  • 您的应用程序尝试打开或访问该文件,例如通过调用openWithCompletionHandler:。
  • 您的应用程序调用NSFileManager方法startDownloadingUbiquitousItemAtURL:错误:明确下载数据。
    因为从iCloud下载大型文件可能会导致显示文档数据的可察觉的延迟,您应该向用户指出下载已经开始(例如,显示“加载”或“更新”),并且该文件当前不是无障碍。下载完成后,请删除该指示。

注意:与文档文件的URL相关联的NSURLUbiquitousItemIsDownloadedKey会告诉您文档当前的下载状态。您可以使用NSURL类的getResourceValue:forKey:error:method来检索此键的值。其他键也可以告诉你有关文件的上传和下载状态的相关信息。有关更多信息,请参阅NSURL类参考。

打开文件

基于示例文档的应用程序在表视图中列出已知文档。当用户点击列出的文档打开它时,UITableView调用其委托的tableView:didSelectRowAtIndexPath:方法。清单4-5所示的这种方法的实现是导航模式的典型:根视图控制器按顺序分配下一个视图控制器 - 在这种情况下,视图控制器显示文档数据,并使用基本数据 - 在这种情况下,文件的文件URL。根据设备成语是iPad还是iPhone(或iPhone touch),根视图控制器将视图控制器添加到拆分视图或将其推送到导航控制器的堆栈上。

清单4-5响应打开文档的请求

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self selectFileAtIndexPath:indexPath create:NO];
}
 
-(void)selectFileAtIndexPath:(NSIndexPath*)indexPath create:(BOOL)create
{
    NSArray* fileList = indexPath.section == 0 ? _localFileList : _ubiquitousFileList;
    DetailViewController* detailViewController = [[DetailViewController alloc]
        initWithFileURL:[[fileList objectAtIndex:indexPath.row] url] createNewFile:create];
 
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        self.splitViewController.viewControllers =
            [NSArray arrayWithObjects:self.navigationController, detailViewController, nil];
    }
    else {
        [self.navigationController pushViewController:detailViewController animated:YES];
    }
    [detailViewController release];
}

在其初始化方法(未显示)中,文档的视图控制器(示例中的DetailViewController)分配UIDocument子类的实例,并通过调用initWithFileURL:方法来初始化它,传递文件URL。 它将新创建的文档对象分配给文档属性。

打开文档的最后一步是调用UIDocument对象上的openWithCompletionHandler:方法; 我们的示例应用程序中的文档视图控制器在viewWillAppear中调用此方法,如清单4-6所示。 代码检查文档状态以验证文档在尝试打开文档之前是否已关闭 - 无需打开已打开的文档。

清单4-6打开文档

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    if (_createFile) {
        [self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating
            completionHandler:^(BOOL success) {
                _textView.text = self.document.text;
        }];
        _createFile = NO;
    }
    else {
        if (self.document.documentState & UIDocumentStateClosed) {
            [self.document openWithCompletionHandler:nil];
        }
    }
}

当openWithCompletionHandler:被调用时,UIDocument从文档文件读取数据,文档对象本身从数据中创建其模型对象。在此操作顺序结束时,执行openWithCompletionHandler:方法的完成处理程序。虽然示例中的视图控制器不实现完成块,但是完成处理程序有时用于将文档数据分配给文档的视图或视图进行显示。 (要回顾什么DetailViewController而不是更新文档视图,请参阅清单3-4及随附的文本。)

关闭文件

要关闭文档,请向文档对象发送一个closeWithCompletionHandler:方法。如果需要,该方法保存文档数据,然后在其唯一参数中执行完成处理程序。

关闭文档的好时机是当文档的视图控制器被关闭时,例如当用户点击后退按钮时。在视图控制器的视图消失之前,将调用viewWillDisappear:方法。您的视图控制器子类可以覆盖此方法,以便调用closeWithCompletionHandler:在文档对象上,如清单4-7所示。

清单4-7关闭文档

-(void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.document closeWithCompletionHandler:nil];
}

将文档移动到iCloud Storage和

如设置文档文件的首选存储位置所述,应用程序应该向其用户提供在本地文件系统(应用程序沙箱)或iCloud(容器目录)中存储所有文档的选项。它将此选项作为用户首选项存储,并在保存和打开文档时引用此首选项。当用户更改首选项时,应用程序应将应用程序沙箱中的所有文档文件移动到iCloud,或者根据更改的性质将所有文件移动到另一个方向。

获取iCloud容器目录的位置

将文档文件从本地存储移动到iCloud容器目录的Documents子目录时,其文件名不变。文件URL路径中唯一不同的部分是文档导出的部分。要获得该路径的一部分,您需要调用NSFileManager的URLForUbiquityContainerIdentifier:方法。大多数情况下,您将无法通过此方法获取应用程序的默认容器目录。如果您的应用程序支持多个容器,则可以通过传递具有相应iCloud容器标识符的字符串来显式地请求容器,该容器标识符是您的团队ID和应用程序包ID的连接,以期间隔开。这些容器标识符字符串与您在Xcode中的应用程序目标“摘要”视图的“标识符”字段中指定的相同。为每个应用程序的容器标识符声明一个字符串常量是个好主意,如下例所示:

static NSString *UbiquityContainerIdentifier = @"A93A5CM278.com.acme.document.billabong”;

清单4-8中的两个方法可以获得iCloud容器标识符,并附加“/ Documents”。

清单4-8获取iCloud容器目录URL

-(NSURL*)ubiquitousContainerURL {

return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

}

-(NSURL*)ubiquitousDocumentsDirectoryURL {

return [[self ubiquitousContainerURL] URLByAppendingPathComponent:@"Documents"];

}

注意:有关获取应用程序沙箱的基本URL的示例,请参阅编写文件URL并保存文档文件。

将文档移动到iCloud存储

以编程方式,您可以通过调用NSFileManager方法setUbiquitous:itemAtURL:destinationURL:error :.将文档放在iCloud存储中。此方法需要应用程序沙箱(源URL)中文档文件的文件URL和应用程序的iCloud容器目录中文档文件的目标文件URL。第一个参数取一个布尔值,应为YES。

重要:您不应该调用setUbiquitous:itemAtURL:destinationURL:error:从应用程序的主线程,特别是如果文档未关闭。因为该方法对指定的文件执行协调的写入操作,所以从主线程调用此方法可能会导致任何文件主持人监视文件的死锁。 (此外,在主线程上执行的此方法可能需要不间断的时间来完成)。而是调用在主线程队列以外的调度队列中运行的块中的方法。调用完成后,您可以随时发送主线程来更新应用程序的其余数据结构。
清单4-9中的方法说明了如何将文档文件从应用程序沙箱移动到iCloud存储。在示例应用程序中,当用户的首选存储位置(iCloud或本地)更改时,将为应用程序沙箱中的每个文档文件调用此方法。这个方法大概有三个部分:

  • 撰写源URL和目标URL。
  • 在二级调度队列中:调用setUbiquitous:itemAtURL:destinationURL:error:method并缓存结果,一个布尔值(成功),指示文档文件是否成功移动到iCloud容器目录。
  • 在主调度队列中:如果调用成功,请更新文档的模型对象及其对象的呈现;如果调用不成功,请记录错误(否则处理它)。
    清单4-9将文档文件从本地存储移动到iCloud存储
- (void)moveFileToiCloud:(FileRepresentation *)fileToMove {

NSURL *sourceURL = fileToMove.url;

NSString *destinationFileName = fileToMove.fileName;

NSURL *destinationURL = [self.documentsDir URLByAppendingPathComponent:destinationFileName];

dispatch_queue_t q_default;

q_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(q_default, ^(void) {

NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];

NSError *error = nil;

BOOL success = [fileManager setUbiquitous:YES itemAtURL:sourceURL

destinationURL:destinationURL error:&error];

dispatch_queue_t q_main = dispatch_get_main_queue();

dispatch_async(q_main, ^(void) {

if (success) {

FileRepresentation *fileRepresentation = [[FileRepresentation alloc]

initWithFileName:fileToMove.fileName url:destinationURL];

[_fileList removeObject:fileToMove];

[_fileList addObject:fileRepresentation];

NSLog(@"moved file to cloud: %@", fileRepresentation);

}

if (!success) {

NSLog(@"Couldn't move file to iCloud: %@", fileToMove);

}

});

});

}

从iCloud存储中删除文档

要将文档文件从iCloud容器目录移动到应用程序沙箱的Documents目录,请按照将文档移动到iCloud Storage中所述的相同过程,但切换源URL(现在是iCloud容器目录中的文档文件)和 目标网址(现在应用程序沙箱中的文档文件)。 另外,setUbiquitous:itemAtURL:destinationURL:error:method的第一个参数现在应该是NO。 清单4-10显示了实现此过程的方法; 它被称为iCloud容器目录中的每个文件,将其移动到应用程序沙箱。

清单4-10将文档文件从iCloud存储移动到本地存储

- (void)moveFileToLocal:(FileRepresentation *)fileToMove {

NSURL *sourceURL = fileToMove.url;

NSString *destinationFileName = fileToMove.fileName;

NSURL *destinationURL = [self.documentsDir URLByAppendingPathComponent:destinationFileName];

dispatch_queue_t q_default;

q_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(q_default, ^(void) {

NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];

NSError *error = nil;

BOOL success = [fileManager setUbiquitous:NO itemAtURL:sourceURL destinationURL:destinationURL

error:&error];

dispatch_queue_t q_main = dispatch_get_main_queue();

dispatch_async(q_main, ^(void) {

if (success) {

FileRepresentation *fileRepresentation = [[FileRepresentation alloc]

initWithFileName:fileToMove.fileName url:destinationURL];

[_fileList removeObject:fileToMove];

[_fileList addObject:fileRepresentation];

NSLog(@"moved file to local storage: %@", fileRepresentation);

}

if (!success) {

NSLog(@"Couldn't move file to local storage: %@", fileToMove);

}

});

});

}

监控文档状态更改和处理错误

文档可以在其运行时间内通过不同的状态。状态可以告诉您文档是否遇到错误,版本冲突或其他不正常的情况。 UIDocument声明常量(类型为UIDocumentState)来表示文档状态,并且当更改是文档的状态发生时,使用这些常量之一设置documentState属性。表4-1列出了状态常数。

表4-1 UIDocumentState常量

文件状态常数 这是什么意思
UIDocumentStateNormal 该文件是开放的,并且没有发生任何冲突或其他问题。
UIDocumentStateClosed 该文件已关闭。如果UIDocument无法打开文档,则文档处于此状态,在这种情况下,文档属性可能无效。
UIDocumentStateInConflict 文档的版本有冲突。
UIDocumentStateSavingError 一个错误会阻止UIDocument保存文档。
UIDocumentStateEditingDisabled 目前还不允许用户编辑文档。

当文档状态发生变化时,UIDocument还会发布类型为UIDocumentStateChangedNotification的通知。您的应用程序应遵守此通知并作出适当响应。文档视图控制器的初始化方法是添加观察者的好地方,如清单4-11所示。这种情况下的观察者是视图控制器。

清单4-11添加UIDocumentStateChangedNotification通知的观察者

-(id)initWithFileURL:(NSURL*)url createNewFile:(BOOL)createNewFile {

NSString* nibName = [[UIDevice currentDevice] userInterfaceIdiom] ==

UIUserInterfaceIdiomPad ? @"DetailViewController_iPad" : @"DetailViewController_iPhone";

self = [super initWithNibName:nibName bundle:nil];

if (self) {

_document = [[ImageNotesDocument alloc] initWithFileURL:url];

// other code here....

[[NSNotificationCenter defaultCenter] addObserver:self

selector:@selector(documentStateChanged)

name:UIDocumentStateChangedNotification object:_document];

}

return self;

}

确保在类的dealloc方法中从通知中心删除观察者。

当文档的状态发生变化时,UIDocument会发布UIDocumentStateChangedNotification通知,通知中心通过调用通知方法(在示例中为documentStateChanged)来发送。 在清单4-12中,观察视图控制器从documentState属性获取当前状态并对其进行评估。 如果状态是UIDocumentStateEditingDisabled,它会隐藏键盘。 如果文档的不同版本(UIDocumentStateInConflict)之间存在冲突,它将在文档视图的工具栏中显示“显示冲突”按钮。 (有关处理文档版本冲突的详细信息,请参阅解决文档版本冲突。)

清单4-12评估当前文档状态

-(void)documentStateChanged {

UIDocumentState state = _document.documentState;

[_statusView setDocumentState:state];

if (state & UIDocumentStateEditingDisabled) {

[_textView resignFirstResponder];

}

if (state & UIDocumentStateInConflict) {

[self showConflictButton];

}

else {

[self hideConflictButton];

[self dismissModalViewControllerAnimated:YES];

}

}

通知处理方法还调用由私有视图类实现的setDocumentState:方法。 如清单4-13所示,该方法会根据文档状态更改文档视图工具栏中的其他项目。

清单4-13更新文档的用户界面以反映其状态

-(void)setDocumentState:(UIDocumentState)documentState {

if (documentState & UIDocumentStateSavingError) {

self.unsavedLabel.hidden = NO;

self.circleView.image = [UIImage imageNamed:@"Red"];

}

else {

self.unsavedLabel.hidden = YES;

if (documentState & UIDocumentStateInConflict) {

self.circleView.image = [UIImage imageNamed:@"Yellow"];

}

else {

self.circleView.image = [UIImage imageNamed:@"Green"];

}

}

}

如果无法保存文档(UIDocumentStateSavingError),则视图控制器将状态指示器更改为红色,并在其旁显示未保存。如果有冲突的文档版本,它会使状态指示灯变黄(这是前面提到的“显示冲突”按钮)。否则,状态指示灯为绿色。

删除文档

就像您想允许用户创建文档一样,您也希望让他们删除所选文档。删除文档需要做三件事:

  • 从存储(从本地沙箱或iCloud容器目录)中删除文档文件。
  • 删除用于表示内存中的文档数据的模型对象。
  • 删除文档视图中显示的文档数据。
    当您从存储中删除文档时,您的代码应该近似UIDocument用于读写操作。它应该在后台队列上异步执行删除,并且应该使用文件协调。清单4-14说明了这个过程。它在后台队列上分派一个任务,创建一个NSFileCoordinator对象,并调用coordinateWritingItemAtURL:options:error:byAccessor:方法。此方法的byAccessor块调用NSFileManager方法来删除文件removeItemAtURL:error :.

清单4-14删除所选文档

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle

forRowAtIndexPath:(NSIndexPath *)indexPath {

NSMutableArray* fileList = nil;

if (indexPath.section == 0) {

fileList = self.localFileList;

}

else {

fileList = self.ubiquitousFileList;

}

NSURL* fileURL = [[fileList objectAtIndex:indexPath.row] url];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {

NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];

[fileCoordinator coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForDeleting

error:nil byAccessor:^(NSURL* writingURL) {

NSFileManager* fileManager = [[NSFileManager alloc] init];

[fileManager removeItemAtURL:writingURL error:nil];

}];

});

[fileList removeObjectAtIndex:indexPath.row];

[tableView deleteRowsAtIndexPaths:[[NSArray alloc] initWithObjects:&indexPath count:1]

withRowAnimation:UITableViewRowAnimationLeft];

}

在此示例中,当表视图处于编辑模式时,当用户点击“删除”按钮时,用户触发该方法的调用。

更改跟踪和撤消操作

UIDocument类的无保存模型功能确保文档数据以频繁的间隔自动保存,减轻用户需要显式保存其文档。 UIDocument实现了无节制模型的许多行为,但是基于文档的应用程序必须发挥自己的作用,才能使功能发挥作用。

UIKit如何自动保存文档数据

用于文档的UIKit框架实现的无节制模型有两个主要部分:用于将文档标记为需要保存的机制以及框架检查该标志时的可变周期。定期地,UIKit调用UIDocument对象的hasUnsavedChanges方法并计算返回的值。如果值为YES,则将文档数据保存到文档文件。 hasUnsavedChanges值检查之间的时间间隔根据几个因素而变化,包括用户输入的速率。

基于文档的应用程序通过实现撤消和重做或跟踪对文档的更改来间接设置hasUnsavedChanges返回的值。更改跟踪需要应用程序调用updateChangeCount:方法,传入UIDocumentChangeDone(UIDocumentChangeKind类型的常量)。当应用程序注册一个撤消操作,然后发送撤消或重做消息到文档的撤销管理器时,UIDocument代表它调用updateChangeCount:。

因为给用户撤消和重做更改的功能可以是一个区别的功能,所以建议大多数应用程序使用该方法。

执行撤消和重做

您可以通过遵循撤消体系结构中的过程和建议,在应用程序中执行撤消和重做操作。请注意,UIDocument定义了一个undoManager属性。您可以通过访问此属性获取默认的NSUndoManager对象,也可以为其分配自己的NSUndoManager对象。 undo管理器必须通过属性与UIDocument对象相关联,以便启用更改跟踪,从而自动保存文档数据。

清单5-1说明了文本字段的撤消和重做的实现。

清单5-1为文本字段实现撤消和重做

- (void)textFieldDidEndEditing:(UITextField *)textField {

self.undoButton.enabled = YES;

self.redoButton.enabled = YES;

if (textField.tag == 1) {

[self setLocationText:textField.text];

}

// code for other text fields here....

}

- (void)setLocationText:(NSString *)newText {

NSString *currentText = _document.location;

if (newText != currentText) {

[_document.undoManager registerUndoWithTarget:self

selector:@selector(setLocationText:)

object:currentText];

_document.location = newText;

self.locationField.text = newText;

}

}

- (IBAction)handleUndo:(id)sender {

[_document.undoManager undo];

if (![_document.undoManager canUndo]) self.undoButton.enabled = NO;

}

- (IBAction)handleRedo:(id)sender {

[_document.undoManager redo];

if (![_document.undoManager canRedo]) self.redoButton.enabled = NO;

}

实施变更跟踪

要实施更改跟踪,而不是执行撤消/重做,请在代码中的适当点调用UIDocument对象上的updateChangeCount:方法。 就像您注册撤消动作一样,通常情况下,您可以使用用户刚刚输入的数据更新文档的模型对象。 传入的参数应该是一个UIDocumentChangeDone常量。

清单5-2显示了如何在文本视图中进行更改时调用的UITextViewDelegate方法中调用updateChangeCount:。

清单5-2更新文档的更改计数

-(void)textViewDidChange:(UITextView *)textView {

_document.documentText = textView.text;

[_document updateChangeCount:UIDocumentChangeDone];

}

解决文件版本冲突

在iCloud世界中,当用户在多个设备或桌面系统上安装了基于文档的应用程序时,同一文档的不同版本之间可能会有冲突。回想一下应用程序会更新本地容器目录中的文档文件,然后这些更改通常会立即发送到iCloud。但是如果这种传播不是即时的呢?例如,您可以使用Mac OS X版本的应用程序编辑文档,但是您也使用iPad版本的应用程序编辑了相同的文档,而您在设备处于“飞行模式”时也是如此。当您关闭飞行模式时,文档的本地更改将传输到iCloud。 iCloud注意到冲突并通知应用程序。

学习文档版本冲突

随着监控文档状态更改和处理错误的描述,您的应用程序通过观察UIDocumentStateChangedNotification通知来了解文档版本冲突。如果documentState属性更改为UIDocumentStateInConflict,则存在相同文档的多个版本。该应用程序负责在有或没有用户帮助的情况下尽快解决这些冲突。

您通过NSFileVersion类的两个类方法了解文档的冲突版本。 currentVersionOfItemAtURL:方法返回一个表示当前文件的NSFileVersion对象;当前文件由iCloud在某些基础上选为当前的“冲突获胜者”,并且在所有设备上都相同。通过调用unresolvedConflictVersionsOfItemAtURL:方法,可以得到一个NSFileVersion对象的数组;这些对象称为冲突版本,每个对象表示位于指定URL的文件的未解决的版本冲突。 NSFileVersion对象可以为您提供有助于解决冲突的信息,例如修改日期,本地化文档名称和保存计算机的本地化名称。

解决文件版本冲突的策略

您的应用程序可以按照解决文档版本冲突的三种策略之一:

  • 合并来自冲突版本的更改。
  • 根据一些相关因素选择其中一个文档版本,例如最新修改日期的版本。
  • 允许用户查看文档的冲突版本,并选择要使用的版本。

哪种策略最适合使用取决于您的文档数据。如果您可以合并不同文档版本的内容,而不引入矛盾元素,那么请遵循该策略。或者如果您的应用程序没有遭受任何数据丢失,请选择具有最新修改日期的文档版本。

一般来说,您应该尝试解决冲突而不涉及用户,但对于某些可能无法实现的应用程序。如果应用程序采用以用户为中心的方法,则应谨慎地通知用户版本冲突,并公开启动解决过程的按钮或其他控件。示例:让用户选择版本检查允许用户选择要使用的文档版本的应用程序的代码。

如何解决iOS文档版本冲突?

当您的应用程序或其用户通过选择文档版本来解决文档版本冲突时,应用程序应完成以下步骤:

  • 如果所选版本是冲突版本,请将当前文档文件替换为冲突版本文档文件。
    为此,请在表示版本的NSFileVersion对象上调用replaceItemAtURL:options:error:方法,传递文档的当前文件URL。

  • 如果所选版本是冲突版本,请还原文档,以便在文档文件中显示新数据
    为此,请调用UIDocument方法revertToContentsOfURL:completionHandler:在文档对象上传入文档的当前文件URL。

  • 将所有冲突版本与文档的文件URL相关联。
    为此,请调用NSFileVersion类方法removeOtherVersionsOfItemAtURL:error :,传递文档的文件URL。

  • 将每个冲突版本标记为已解决,以便iOS不再作为冲突版本提高它。
    为此,将表示冲突版本的每个NSFileVersion对象的已解析属性设置为YES。这一步应该始终如一地完成。

  • 删除文档的已解析版本。

对于您不再需要的任何版本,请调用NSFileVersion的removeAndReturnError:方法来回收该文件的存储。删除文档修订版本将保留在服务器上。

一个例子:让用户选择版本

我们的基于文档的应用程序是一个简单的文本编辑器。这样的应用程序难以在文档的冲突版本中定位和合并文本差异,即使如此,生成的文档也可能不是用户想要的。应用程序可以选择最近修改日期的文档版本,但是再次无法确定用户想要的版本。在这种情况下,一个很好的解决冲突的策略是让最熟悉文档内容的用户选择自己想要的版本。

您可能会从监控文档状态更改和处理错误中记住清单6-1中所示的代码。此代码显示文档视图控制器的方法,处理UIDocument在文档状态发生更改时发布的UIDocumentStateChangedNotification通知。如果新文档状态为UIDocumentStateInConflict,则视图控制器在自定义状态视图中显示“解决冲突”按钮。 (它还将状态指示灯的颜色设置为红色。)

清单6-1检测文档版本中的冲突

-(void)documentStateChanged {

UIDocumentState state = _document.documentState;

[_statusView setDocumentState:state];

if (state & UIDocumentStateEditingDisabled) {

[_textView resignFirstResponder];

}

if (state & UIDocumentStateInConflict) {

[self showConflictButton]; // <------ Shows "Resolve Conflicts" button

}

else {

[self hideConflictButton];

[self dismissModalViewControllerAnimated:YES];

}

}

当用户点击按钮时,UIKit将调用清单6-2中的方法。 此方法以模式显示自定义冲突解决程序视图控制器的视图。

清单6-2显示用于解决文档版本冲突的用户界面

-(void)conflictButtonPushed

{

ConflictResolverViewController* conflictResolver = [[ConflictResolverViewController alloc]

initWithURL:_document.fileURL delegate:self];

[self presentViewController:conflictResolver animated:YES completion:nil];

[conflictResolver release];

}

当用户点击按钮时,UIKit将调用清单6-2中的方法。 此方法以模式显示自定义冲突解决程序视图控制器的视图。

清单6-2显示用于解决文档版本冲突的用户界面…

-(void)conflictResolver:(ConflictResolverViewController *)conflictResolver

didResolveWithFileVersion:(NSFileVersion *)fileVersion {

[self dismissViewControllerAnimated:YES completion:nil];

[fileVersion replaceItemAtURL:_document.fileURL options:0 error:nil];

[NSFileVersion removeOtherVersionsOfItemAtURL:_document.fileURL error:nil];

[_document revertToContentsOfURL:_document.fileURL completionHandler:nil];

NSArray* conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:_document.fileURL];

for (NSFileVersion* fileVersion in conflictVersions) {

fileVersion.resolved = YES;

}

}

-(void)conflictResolverDidResolveWithCurrentVersion:(ConflictResolverViewController*)conflictResolver {

[self dismissViewControllerAnimated:YES completion:nil];

[NSFileVersion removeOtherVersionsOfItemAtURL:_document.fileURL error:nil];

NSArray* conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:_document.fileURL];

for (NSFileVersion* fileVersion in conflictVersions) {

fileVersion.resolved = YES;

}

}

这些方法说明了如何解决iOS文档版本冲突的步骤。如果选择的文档是冲突版本,则代理将在传入的NSFileVersion对象中调用replaceItemAtURL:options:error:将iCloud容器目录中的文档文件替换为所选文档。然后委托枚举包含表示文档的所有冲突版本的NSFileVersion对象的数组,并将每个对象的已解析属性设置为YES。然后,它要求NSFileVersion删除与文档的文件URL相关联的文档的所有其他冲突版本,并调用revertToContentsOfURL:completionHandler:将显示的文档还原到文档文件的新内容。

选择当前文档文件时调用的第二个委托方法要简单得多。它将表示冲突版本的所有NSFileVersion对象的已解析属性设置为YES,并删除与文档文件URL相关联的所有冲突版本。

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

推荐阅读更多精彩内容