NSKeyedArchiver и совместное использование пользовательского класса между целями

В моем приложении в качестве модели данных используется пользовательский класс:

class Drug: NSObject, NSCoding {
    // Properties, methods etc...
}

Я только что создал расширение «Сегодня», и мне нужно получить доступ к данным пользователя из него, поэтому я использую NSCoding для сохранения своих данных как в контейнере приложения, так и в общем контейнере. Это функции сохранения и загрузки в основном приложении:

func saveDrugs() {
    // Save to app container
    let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
    if isSuccessfulSave {
        print("Drugs successfully saved locally")
    } else {
        print("Error saving drugs locally")
    }

    // Save to shared container for extension
    let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
    if isSuccessfulSaveToSharedContainer {
        print("Drugs successfully saved to shared container")
    } else {
        print("Error saving drugs to shared container")
    }
}

func loadDrugs() -> [Drug]? {
    return NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
}

Я столкнулся с проблемой пространства имен классов, когда NSKeyedUnarchiver в моем расширении Today не мог правильно декодировать объект, поэтому я использовал этот ответ и добавил @objc перед определением класса:

@objc(Drug)
class Drug: NSObject, NSCoding {
    // Properties, methods etc...
}

Это отлично решило проблему. Однако это будет версия 1.3 моего приложения, и кажется, что это нарушает процесс разархивирования ранее существовавших данных (как я и думал).

Как лучше всего справиться с этим сценарием, если я просто внесу это изменение, новая версия приложения выйдет из строя для существующих пользователей!

Я не могу найти никаких других ответов по этому поводу, и я не уверен, что метод NSKeyedArchiver.setClass() актуален, и я не уверен, где его использовать.

Любая помощь будет принята с благодарностью. Спасибо.


person Chris    schedule 02.02.2018    source источник
comment
Почему приложение вылетает?   -  person matt    schedule 02.02.2018
comment
Привет, @matt. Он вылетает из-за того, что старый класс Drug (myApp.Drug) не может быть декодирован NSKeyedUnarchiver после добавления строки @objc(Drug) — класс теперь становится *.Drug, который работает при совместном использовании между приложением и расширением, но не декодирует старые пользовательские данные. Новые пользователи были бы в порядке.   -  person Chris    schedule 02.02.2018


Ответы (1)


Это именно тот случай использования для NSKeyedUnarchiver.setClass(_:forClassName:) — вы можете перенести старые классы вперед связывая текущие классы с их старыми именами:

let unarchiver = NSKeyedUnarchiver(...)
unarchiver.setClass(Drug.self, forClassName: "myApp.Drug")
// unarchive as appropriate

В качестве альтернативы вы можете предоставить делегата, соответствующего NSKeyedUnarchiverDelegate и предоставляющего unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:), но для данного сценария это, скорее всего, излишне.


Имейте в виду, что это работает для переноса старых данных вперед. Если у вас есть более новые версии приложения, которым необходимо отправлять данные в старые версии приложения, вам нужно будет сделать то же самое на стороне архива — продолжайте кодировать новый класс со старым именем с NSKeyedArchiver.setClassName(_:for:):

let archiver = NSKeyedArchiver(...)
archiver.setClassName("myApp.Drug", for: Drug.self)
// archive as appropriate

Если у вас есть проблема с обратной совместимостью, то, к сожалению, вам, вероятно, придется продолжать использовать старое имя, пока есть пользователи старой версии приложения.

person Itai Ferber    schedule 02.02.2018
comment
Спасибо тебе за это! Мне нужно только разархивировать, так как моему расширению Today нужно только читать данные, а не записывать их. Как только пользователь обновит приложение и загрузит свои сохраненные данные, они будут повторно заархивированы по-новому. Просто чтобы уточнить, будет ли ваша реализация разархивировать как новый класс (@objc(Drug)), так и старый? Мне все еще нужно использовать @objc? - person Chris; 02.02.2018
comment
Я сейчас не за своим Mac, но я проверю это позже и отмечу ваш ответ как правильный. Спасибо. - person Chris; 02.02.2018
comment
@Ampoule Вам все равно нужно будет дать классу имя @objc, чтобы он оставался стабильным между обеими целями, поэтому оставьте @objc(Drug). Установка класса для существующего имени класса является аддитивной — она добавляет сопоставление от "myApp.Drug" к Drug и позволяет вам разархивировать "Drug" как самого себя. - person Itai Ferber; 02.02.2018
comment
Я попробовал это, но он не скомпилируется, так как я пытаюсь использовать метод unArchiveObject экземпляра unarchiver, но это статический метод, поэтому его нужно вызывать в NSKeyedUnarchiver. Однако как мне тогда использовать setClass? Могу ли я сделать NSKeyedUnarchiver.setClass(...) затем NSKeyedUnarchiver.unarchiveObject(...)? - person Chris; 03.02.2018
comment
@Chris Это один из способов сделать это. Существует функция класса с эквивалентным именем, которая позволяет все экземпляры NSKeyedUnarchiver. Таким образом, вы должны написать NSKeyedUnarchiver.setClass(Drug.self, forClassName: "myApp.Drug"), а затем разархивировать с помощью NSKeyedUnarchiver.unarchiveObject(with: ...). - person Itai Ferber; 03.02.2018
comment
Ах, теперь я вижу. Только что проверил документацию Apple, и я вижу, что есть методы класса и экземпляра. Я явно выбирал только метод экземпляра в XCode. Спасибо еще раз! - person Chris; 03.02.2018
comment
@Chris Рад, что помог. :) - person Itai Ferber; 03.02.2018
comment
Простите, а куда вы кладете метод unarchiver? на классе? или appdelegate?, я говорю об этом: let archiver = NSKeyedArchiver(...) archiver.setClassName(myApp.Drug, for: Drug.self) - person Alejandro Viquez; 09.01.2019
comment
@AlejandroViquez Где бы вы ни собирались действовать и архивировать или разархивировать данные. Таким образом, в зависимости от того, какие подпрограммы обрабатывают либо сохранение данных на диск, либо координацию этого. Это будет зависеть от того, как вы настроили это в своем приложении. Кроме того, NSKeyedArchiver и NSKeyedUnarchiver имеют методы класса, выполняющие одно и то же действие (например, NSKeyedArchiver.setClass(_:forClassName:), которые влияют на все архивы в приложении, и вы можете вызывать из AppDelegate или аналогичного - person Itai Ferber; 10.01.2019