Как лучше всего справиться с восстановлением состояния CoreData + iOS?

Я пытаюсь добавить восстановление состояния iOS 6 в приложение, с которым я почти закончил. Это приложение, в котором модель в основном взята из CoreData.

Как рекомендовано, я использую Подход «передать эстафету» к перемещению контекстов управляемых объектов между контроллерами представления - я создаю MOC в своем делегате приложения, передаю его первому контроллеру представления, который передает его второму в prepareForSegue :, который передает его третьему в prepareForSegue :, так далее.

Кажется, это не очень хорошо сочетается с State Restoration. Единственное, что я могу придумать, - это получить MOC из моего делегата приложения непосредственно в реализации viewControllerWithRestorationIdentifierPath: coder :. Фактически, похоже, что разработчики Apple сделали нечто подобное при просмотре сеанса WWDC.

Это лучший / единственный способ? Действительно ли State Restoration нарушает работу Pass-The-Baton, по крайней мере, для восстанавливаемых контроллеров представления?


person bpapa    schedule 31.07.2013    source источник
comment
Вы нашли решение этого? Это хороший вопрос, и я тоже думаю о том, как лучше его решить.   -  person Anton    schedule 27.08.2013
comment
Антон - не совсем. Я просто начал думать об этом как об особом случае и пошел с первоначальной идеей напрямую ссылаться на делегата приложения. Я оставлю вопрос открытым, чтобы понять, что пытаются другие люди.   -  person bpapa    schedule 27.08.2013
comment
@ Matthias-Bauch Я давно не смотрел на этот материал, но полагаю, что либо в 2014 году был сеанс WWDC под названием «Что нового в восстановлении состояния», в котором были некоторые новые функции, и, похоже, это могло бы помочь здесь.   -  person bpapa    schedule 30.04.2015
comment
NSManagedObjectContext соответствует протоколу NSCoding. Конечно, я не пробовал этого, но похоже, что вы сможете его кодировать, а затем декодировать во всех восстанавливаемых VC.   -  person Stakenborg    schedule 05.05.2015


Ответы (6)


Чтобы ознакомиться с восстановлением состояния, я настоятельно рекомендую сеанс WWDC 2013 Что нового в восстановлении состояния . В то время как восстановление состояния было введено годом ранее в iOS 6, iOS 7 внесла некоторые заметные изменения.

Передача вперед

Используя подход «передать дубинку», в какой-то момент создается корень NSManagedObjectContext и прикрепляется NSPersistentStoreCoordinator. Контекст передается контроллеру представления, а последующие контроллеры дочернего представления, в свою очередь, передают этот корневой контекст или дочерний контекст.

Например, когда пользователь запускает приложение, корневой NSManagedObjectContext создается и передается контроллеру корневого представления, который управляет NSFetchedResultsController. Когда пользователь выбирает элемент в контроллере представления, создается новый контроллер подробного представления и передается экземпляр NSManagedObjectContext.

Передача дубинки контекста управляемого объекта

Сохранение и восстановление состояния

Восстановление состояния меняет это способом, который важен для приложений, использующих Core Data в контроллерах представления. Если пользователь находится на контроллере подробного представления и отправляет приложение в фоновый режим, система создает архив восстановления с информацией, полезной для восстановления состояния, видимого, когда они ушли. Информация обо всей цепочке контроллеров представления записывается, и при перезапуске приложения она используется для восстановления состояния.

Восстановление состояния

Когда это происходит, он не использует никаких настраиваемых инициализаторов, сегментов и т. Д. Протокол 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 от координатора постоянного хранилища.
  3. Получить экземпляр управляемого объекта из контекста управляемого объекта для этого идентификатора управляемого объекта

Например:

- (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.

Pass the Baton vs. State Restoration: FIGHT!

При подходе «передать эстафету» был создан экземпляр контроллера представления в результате взаимодействия с пользователем, и в него был передан контекст управляемого объекта. Этот контекст управляемого объекта был связан с родительским контекстом или постоянным координатором хранилища.

Во время восстановления состояния этого не происходит. Если вы посмотрите на иллюстрации того, что происходит во время передачи эстафеты против восстановления состояния, они могут выглядеть очень похожими - и так оно и есть. Во время восстановления состояния передаются данные - экземпляр NSCoder, представляющий интерфейс к архиву восстановления.

К сожалению, необходимая нам NSManagedObjectContext информация не может быть сохранена как часть архива восстановления. NSManagedObjectContext соответствует NSCoding, но важные части не соответствуют. NSPersistentStoreCoordinator нет, поэтому он не будет сохраняться. Любопытно, что свойство parentContext у NSManagedObjectContext также не будет (я настоятельно рекомендую зарегистрировать радар по этому поводу). Сохранение URL-адресов конкретных NSPersistentStore экземпляров и воссоздание 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, реализация по умолчанию перейдет к делегату приложения, чтобы вернуть результат, если какой-либо другой объект по пути не предоставил результат, отличный от нуля.

У вас также есть возможность использовать фабрики классов восстановления с восстановлением состояния для восстановления цепочки контекстов управляемых объектов во время восстановления.

Эти решения подходят не для каждого приложения или ситуации, только вы можете решить, что подойдет вам.

person quellish    schedule 07.05.2015
comment
Использование цепочки респондентов - отличная идея, но на практике я не думаю, что это сработает. Причина в том, что вам часто нужен контекст в viewDidLoad или viewWillAppear, и в любом из этих случаев еще нет nextResponder. Однако targetViewControllerForAction из образца Apple AdaptivePhotos может быть хорошей альтернативой, потому что parentViewController действителен в viewDidLoad (при загрузке из раскадровки) и всегда действителен в viewWillAppear. Однако вам нужно будет установить контекст для rootViewController в делегате приложения. - person malhal; 11.11.2019

Я думаю, что лучший способ справиться с этим - это закодировать MOC в:

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder

а затем декодировать при восстановлении с помощью:

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder

Это должно сохранять эстафету между восстановлением состояния.

Имейте в виду, что каждый VC, использующий MOC, должен реализовать это, если вы придерживаетесь такого подхода.

Чтобы немного расширить, используйте метод + (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder для инициализации вашего VC, затем MOC должен быть декодирован с помощью метода, упомянутого выше, и все должно быть настроено.

Это, надеюсь, предоставит достаточно информации для помогут вам кодировать и декодировать любую информацию, которую вы хотите восстановить при восстановлении.

person Stakenborg    schedule 05.05.2015
comment
NSManagedObjectContext действительно поддерживает NSCoding. На самом деле это звучит очень странно. Я не уверен, что он делает. Придется проверить, поможет ли это завтра утром. - person Matthias Bauch; 05.05.2015
comment
К сожалению, это так, как я ожидал, при архивировании контекста сохраняется пара основных значений (например, concurrencyType, stalenessInterval, name, mergePolicy, ...), но не сохраняется весь стек. Я, честно говоря, не знаю, как это вообще возможно, потому что такие вещи, как parentContexts, должны быть разделены между разными childContexts. Стек CoreData сам по себе представляет собой граф объектов. Таким образом, архивирование контекста просто переводит проблему на другой уровень, потому что мне все еще нужно выяснить, должен ли и какой persistentStoreCoordinator или parentContext должен быть неархивированный контекст. - person Matthias Bauch; 06.05.2015
comment
Ха! UIApplicationDelegate имеет метод application(application:viewControllerWithRestorationIdentifierPath:coder:) , в котором я могу централизованно создавать viewControllers. Поскольку это должно вызываться в правильном порядке, я должен иметь возможность сохранять ссылку на каждый отдельный viewController. Теперь мне просто нужно сохранить инструкции, как воссоздать иерархию контекста. например Получите parentContext из viewController с идентификатором восстановления Foo.Bar.Baz Объединение этого с архивированием контекста действительно кажется выполнимым. - person Matthias Bauch; 06.05.2015

Я не сделал тонны с восстановлением состояния, но я бы подумал в этом направлении:

  • Просыпается ли делегат приложения первым? Есть ли возможность для делегата приложения обойти контроллеры представления?

  • Может ли контроллер представления приостановить, пока он ждет, пока AppDelegate предоставит ему контекст?

Похоже, что восстановление состояния может быть особым случаем но я бы исследовал возможность сделать контроллеры представления достаточно умными, чтобы дождаться появления MOC, прежде чем запрашивать данные. Возможно, даже наличие состояния отката в контроллерах представления, когда они возвращаются в место, где контроллер представления может ждать контекста.

person Marcus S. Zarra    schedule 14.01.2014

Подкласс NSPersistentContainer (как состояние документов); принять протокол UIStateRestoring, чтобы его можно было зарегистрировать с восстановлением состояния, которое позволяет указателю быть доступным во время методов восстановления, сам объект фактически не кодируется в архиве. Вызов registerObjectForStateRestoration UIApplication внутри ленивого persistentContainer геттера. Также, когда состояние документов передает persistentContainer контроллерам представления в application:willFinishLaunching и prepareForSegue, а не только контекст. Для контроллеров представления, которые не могут передать контейнер в willFinishLaunching, закодируйте persistentContainer и URI объекта в encodeRestorableStateWithCoder виртуального канала. Для показа (т.е. push) виртуальных каналов, которые помещаются в стек навигации главного устройства, используйте технику restorationClass и метод класса UIViewController viewControllerWithRestorationIdentifierPath и декодируйте persistentContainer, а затем с помощью existingObjectWithID верните ноль, если объект больше не существует, что препятствует созданию виртуального канала, если он существует, затем инициализирует VC, используя закодированную раскадровку и объект. В случае отображения подробных VC, которые всегда создаются независимо от существующего объекта, нет необходимости кодировать persistentContainer и нет необходимости в дизайне класса восстановления, просто реализуйте application:viewControllerWithRestorationIdentifierPath: делегата приложения и используйте persistentContainer делегата приложения и установите объект на контроллер детального представления из начальной раскадровки (захватите его в свойстве в application:willFinishedLaunching и очистите его после завершения восстановления, причина в том, что разделение может разрушиться до вызова application:viewControllerWithRestorationIdentifierPath:, то есть его нельзя получить через окно).

Причина, по которой мы не декодируем объект в методе decodeRestorableStateWithCoder, заключается в том, что он вызывается после viewDidLoad, что слишком поздно для установки того, что нам нужно.

person malhal    schedule 17.07.2018

Я узнал об одном очень понятном способе настройки стека Core Data из NSScreencast. По сути, вы запускаете свой проект Xcode, не выбирая вариант «Использовать основные данные». Затем вы добавляете одноэлементный класс, который является вашей моделью данных. Итак, чтобы получить основной MOC, вы должны сделать [[DataModel sharedModel] mainContext]. Я считаю, что это намного чище, чем выгрузить все в App Delegate.

Я никогда не использовал это таким образом, но я думаю, что в вашем случае вы также можете сделать это в своих контроллерах представления:

-(NSManagedObjectContext*)moc
{ 
    if (_moc != nil) return _moc;
    _moc = [[DataModel sharedModel] mainContext];
    return _moc;
}
person Shinigami    schedule 08.11.2013
comment
Единственное, чего здесь не хватает, - это безопасность потоков. Если отправитель этого сообщения не находится в основном потоке, вы должны создать контекст для каждого потока, вместо того, чтобы разделять один контекст между потоками. - person Patrick Goley; 08.11.2013
comment
Спасибо, но это не имеет никакого отношения к восстановлению состояния. Вопрос не в том, как настроить контекст управляемого объекта, а в том, как заставить его хорошо работать с восстановлением состояния. - person bpapa; 09.11.2013
comment
К вашему сведению, это обмен одного синглтона на другой. Если вы собираетесь использовать синглтон, вы можете оставить его в App Delegate. - person Marcus S. Zarra; 14.01.2014
comment
Я просто никогда не понимал, почему материал Core Data вообще принадлежит делегату приложения. Мне нравится эта одноэлементная сделка, потому что у меня ОКР;) - person Shinigami; 14.01.2014
comment
@Shinigami использование синглтона для CD - это то, что сначала звучит как хорошая идея, но по мере того, как ваше приложение становится более сложным, вы понимаете, почему вы не хотите этого делать. Когда вы начинаете раскручивать другие MOC, это становится немного неудобно. - person bpapa; 20.04.2014

Мое решение заключалось в том, чтобы по умолчанию контроллеры представления использовали глобальный общий MOC. Этот контекст будет предполагаемым в большинстве случаев, и если вам нужно передать любой другой MOC, это вполне возможно сделать.

Такой подход подтверждает подход Apple «передать эстафету», а также удобен и совместим с восстановлением состояния.

person Anton    schedule 30.04.2014
comment
Нет, глобальный общий объект был бы противоположностью подхода «передать эстафету». - person quellish; 03.05.2015