25

我正在尝试将 iOS 6 状态恢复添加到我即将完成的应用程序中。这是一个模型主要来自 CoreData 的应用程序。

按照建议,我使用“传递接力棒”的方法在视图控制器之间移动托管对象上下文 - 我在我的 App Delegate 中创建 MOC,将其传递给第一个视图控制器,后者将其传递给 prepareForSegue 中的第二个:,它将它传递给 prepareForSegue: 等中的第三个。

这似乎与状态恢复不太吻合。我唯一能想到的就是在 viewControllerWithRestorationIdentifierPath:coder: 的实现中直接从我的 App Delegate 中检索 MOC。事实上,苹果开发者在观看 WWDC 会议时似乎也做了类似的事情。

这是最好/唯一的方法吗?状态恢复是否有效地打破了传递接力棒,至少对于恢复的视图控制器?

4

6 回答 6

9

为了熟悉状态恢复,我强烈推荐WWDC 2013 会议 What's New in State Restoration。虽然一年前在 iOS 6 中引入了状态恢复,但 iOS 7 带来了一些显着的变化。

向前传递

使用“传递接力棒”的方法,在某些时候NSManagedObjectContext会创建一个根并NSPersistentStoreCoordinator附加一个。上下文被传递给视图控制器,随后的子视图控制器依次传递该根上下文或子上下文。

例如,当用户启动应用程序时,NSManagedObjectContext会创建根并将其传递给根视图控制器,该控制器管理NSFetchedResultsController. 当用户在视图控制器中选择一个项目时,会创建一个新的详细视图控制器并NSManagedObjectContext传入一个实例。

传递托管对象上下文指挥棒

保存和恢复状态

状态恢复以对在视图控制器中使用 Core Data 的应用程序具有重要意义的方式改变了这一点。如果用户在详细视图控制器上并将应用程序发送到后台,系统会创建一个恢复档案,其中包含用于重建他们离开时可见状态的信息。有关整个视图控制器链的信息被写出,当应用程序重新启动时,这些信息用于重建状态。

状态恢复

发生这种情况时,它不使用任何自定义初始化程序、segue 等。UIStateRestoring协议定义了用于编码和解码状态的方法,这些方法允许一定程度的定制。符合的对象NSCoding可以存储在恢复档案中,并且在 iOS 7 状态恢复被扩展到模型对象和数据源。

状态恢复旨在仅存储重建应用程序可见状态所需的信息。对于 Core Data 应用程序,这意味着将定位对象所需的信息存储在正确的持久存储中。

从表面上看,这似乎很简单。在管理视图控制器的情况下,NSFetchedResultsController这可能意味着存储谓词和排序描述符。对于显示或编辑单个托管对象的详细视图控制器,托管对象的 URI 表示将添加到状态恢复存档中:

- (void) encodeRestorableStateWithCoder:(NSCoder *)coder {
    NSManagedObjectID   *objectID   = [[self managedObject] objectID];

    [coder encodeObject:[objectID URIRepresentation] forKey:kManagedObjectKeyPath];
    [super encodeRestorableStateWithCoder:coder];
}

当状态恢复时,调用 UIStateRestoring 方法-decodeRestorableStateWithCoder:从归档信息中恢复对象:

  1. 从恢复存档中解码 URI。
  2. 从持久存储协调器获取 URI 的托管对象 ID
  3. 从该托管对象 ID 的托管对象上下文中获取托管对象实例

例如:

- (void) decodeRestorableStateWithCoder:(NSCoder *)coder {
    NSURL               *objectURI  = nil;
    NSManagedObjectID   *objectID   = nil;
    NSPersistentStoreCoordinator    *coordinator    = [[self managedObjectContext] persistentStoreCoordinator];

    objectURI = [coder decodeObjectForKey:kManagedObjectKeyPath];
    objectID = [coordinator managedObjectIDForURIRepresentation:objectURI];
    [[self managedObjectContext] performBlock:^{
        NSManagedObject *object = [self managedObjectContext] objectWithID:objectID];
        [NSOperationQueue mainQueue] addOperationWithBlock:^{
            [self setManagedObject:object];
        }];
    }]; 
}

这就是事情变得更加复杂的地方。在应用程序生命周期中-decodeRestorableStateWithCoder:调用视图控制器的那一刻,将需要正确 NSManagedObjectContext的.

传递接力棒与状态恢复:战斗!

使用“传递接力棒”方法,视图控制器作为用户交互的结果被实例化,并传入一个托管对象上下文。该托管对象上下文连接到父上下文或持久存储协调器。

在状态恢复期间不会发生。如果您查看“传递接力棒”与状态恢复期间发生的情况的插图,它们可能看起来非常相似 - 确实如此。在状态恢复期间,数据传递——NSCoder代表恢复档案接口的实例。

不幸的是,NSManagedObjectContext我们需要的信息不能作为恢复档案的一部分存储。NSManagedObjectContext 确实符合NSCoding,但重要部分不符合。NSPersistentStoreCoordinator没有,所以它不会被持久化。奇怪的是,parentContextan 的属性NSManagedObjectContext也不会(我强烈建议对此提出雷达)。NSPersistentStore存储特定实例的 URL并NSPersistentStoreCoordinator在每个视图控制器中重新创建一个似乎很有吸引力的选择,但结果将是每个视图控制器的协调器不同 - 这可能很快导致灾难。

因此,虽然状态恢复可以提供在 中定位实体所需的信息NSManagedObjectContext,但它不能直接提供重新创建上下文本身所需的信息。

那么接下来呢?

最终,视图控制器中需要的是一个与状态编码时具有相同父-decodeRestorableStateWithCoder:级的实例。NSManagedObjectContext它应该具有相同的祖先上下文和持久存储结构。

状态恢复从 UIApplicationDelegate 开始,其中几个委托方法作为恢复过程的一部分被调用 ( -application:willFinishLaunchingWithOptions:, -application:shouldRestoreApplicationState:, -didDecodeRestorableStateWithCoder:, -application:viewControllerWithRestorationIdentifierPath:coder:)。其中每一个都是从一开始就自定义恢复过程并传递信息的机会 - 例如将NSManagedObjectContext实例作为关联对象引用附加到NSCoder用于恢复的对象。

如果应用程序委托对象负责创建根上下文,则一旦启动过程完成(有或没有状态恢复),该对象可以在整个视图控制器链中下推。每个视图控制器都会将适当的NSManagedObjectContext实例传递给它的子视图控制器:

@implementation UIViewController (CoreData)

- (void) setManagedObjectContext:(NSManagedObjectContext *)context {
    [[self childViewControllers] makeObjectsPerformSelector:_cmd withObject:context];
}

@end

并且每个提供它自己的实现的视图控制器都会创建它自己的子上下文。这还有其他优点 - 任何让托管对象上下文的用户对其更改做出反应的方法都可以更轻松地异步创建上下文。创建上下文本身是快速且轻量级的,但是将持久存储添加到根上下文可能非常昂贵,并且不应允许在主队列上运行。许多应用程序在应用程序委托方法中的主队列上执行此操作,并且在打开存储的文件时间过长或需要迁移时最终被操作系统杀死。在另一个线程上添加持久性存储,然后在准备好时将上下文发送给使用它的对象可以帮助防止此类问题。

另一种方法可能是利用视图控制器中的响应者链。在状态恢复期间,视图控制器可以遍历响应者链以找到链上的下一个NSManagedObjectContext,创建子上下文并使用它。使用非正式协议实现这一点很简单,并且会产生一个灵活且适应性强的解决方案。

非正式协议的默认实现将在响应者链上走​​得更远:

@implementation UIResponder (CoreData)

- (NSManagedObjectContext *) managedObjectContext {
    NSManagedObjectContext    *result = nil;

    if ([self nextResponder] != nil){
        if ([[self nextResponder] respondsToSelector:@selector(managedObjectContext)]){
            result = [[self nextResponder] managedObjectContext];
        }
    }
    return result;
}

@end

并且响应者链中的任何对象都可以实现-managedObjectContext以提供替代实现。这包括应用程序委托,它确实参与了响应者链。使用上面的非正式协议,如果视图或视图控制器调用-managedObjectContext默认实现将一直到应用程序委托以返回结果,除非沿途有其他对象提供非零结果。

您还可以选择使用具有状态恢复的恢复类工厂来在恢复期间重建托管对象上下文链。

这些解决方案并不适用于所有应用程序或情况,只有您可以决定什么对您有用。

于 2015-05-07T02:50:26.990 回答
3

我认为处理此问题的最佳方法是将 MOC 编码为:

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder

然后在通过以下方式恢复时解码:

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder

这应该保留在状态恢复之间传递接力棒的方法。

请记住,如果您采用这种方法,每个使用 MOC 的 VC 都应该实现这一点。

稍微扩展一下,使用+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder方法来初始化你的VC,然后MOC应该通过上面提到的方法解码,你应该已经设置好了。

应该有望提供足够的信息,让您在恢复时对您想要恢复的任何信息进行编码和解码。

于 2015-05-05T14:12:31.930 回答
2

我没有做很多状态恢复,但我会沿着这些思路思考:

  • 应用程序委托是否首先被唤醒?应用程序委托是否有机会遍历视图控制器?

  • 视图控制器可以在等待 AppDelegate 为其提供上下文时暂停吗?

听起来状态恢复可能是一个特例,但我会探索使视图控制器足够智能以在请求数据之前等待 MOC 出现的选项。甚至可能在视图控制器中有回滚状态,它们会退回到视图控制器可以等待上下文的位置。

于 2014-01-14T19:02:19.123 回答
0

子类NSPersistentContainer(如文档所述);采用该UIStateRestoring协议,因此它可以注册状态恢复,这允许在恢复方法期间访问指针,对象本身实际上并未在存档中编码。CallUIApplicationregisterObjectForStateRestoration惰性persistentContainer吸气剂中。此外,文档状态将 传递给andpersistentContainer中的视图控制器,而不仅仅是上下文。对于无法获取容器传递的视图控制器,将和对象的 URI编码到 VC 的. 对于在主导航堆栈上推送的显示(即推送)VC,使用技术和类方法 viewControllerWithRestorationIdentifierPath 并解码application:willFinishLaunchingprepareForSeguewillFinishLaunchingpersistentContainerencodeRestorableStateWithCoderrestorationClassUIViewControllerpersistentContainer然后使用existingObjectWithIDreturn nil 如果对象不再存在,这会阻止创建 VC,如果它存在,则使用编码的故事板和对象初始化 VC。在显示细节VC的情况下,无论对象是否存在,都不需要编码persistentContainer也不需要恢复类设计,只需实现应用程序委托application:viewControllerWithRestorationIdentifierPath:并使用应用程序委托persistentContainer并在详细视图上设置对象初始情节提要中的控制器(在属性中捕获它application:willFinishedLaunching并在完成恢复时将其清除,原因是拆分可以在application:viewControllerWithRestorationIdentifierPath:调用之前折叠,这意味着它无法通过窗口检索)。

我们不在decodeRestorableStateWithCoder方法中解码对象的原因是因为在调用之后viewDidLoad为时已晚,无法设置我们需要的东西。

于 2018-07-17T14:50:21.427 回答
-2

我从NSScreencast学到了一种非常简洁的设置核心数据堆栈的方法。基本上,您在没有选择“使用核心数据”选项的情况下启动您的 Xcode 项目。然后添加一个作为数据模型的单例类。所以要获得主要的 MOC,你会做[[DataModel sharedModel] mainContext]. 我发现将所有内容都倾倒在 App Delegate 中要干净得多。

我从来没有这样用过,但我想在你的情况下你也可以在你的视图控制器中这样做:

-(NSManagedObjectContext*)moc
{ 
    if (_moc != nil) return _moc;
    _moc = [[DataModel sharedModel] mainContext];
    return _moc;
}
于 2013-11-08T19:32:03.847 回答
-2

我对此的解决方案是让视图控制器默认使用全局共享 MOC。在大多数情况下,此上下文将是预期的上下文,如果您需要通过任何其他 MOC,则完全有可能这样做。

这种方法证实了 Apple 的“传递接力棒”方法,并且既方便又兼容状态恢复。

于 2014-04-30T09:43:05.063 回答