Как использовать Swift 4 Codable в Core Data?

Codable кажется очень интересной особенностью. Но мне интересно, как мы можем использовать это в Core Data? В частности, возможно ли напрямую кодировать / декодировать JSON из / в NSManagedObject?

Я пробовал очень простой пример:

введите описание изображения здесь

и определил Foo себя:

import CoreData

@objc(Foo)
public class Foo: NSManagedObject, Codable {}

Но при таком использовании:

let json = """
{
    "name": "foo",
    "bars": [{
        "name": "bar1",
    }], [{
        "name": "bar2"
    }]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let foo = try! decoder.decode(Foo.self, from: json)
print(foo)

Компилятор вышел из строя с этой ошибкой:

super.init isn't called on all paths before returning from initializer

а целевым файлом был файл, определяющий Foo

Наверное, я сделал это неправильно, так как даже не сдал NSManagedObjectContext, но я понятия не имею, куда его приклеить.

Поддерживает ли Core Data Codable?


person hgl    schedule 09.06.2017    source источник
comment
Хороший пример, в котором используется принятый ответ, можно найти здесь   -  person Honey    schedule 31.12.2018


Ответы (4)


Вы можете использовать интерфейс Codable с объектами CoreData для кодирования и декодирования данных, однако он не такой автоматический, как при использовании с простыми старыми быстрыми объектами. Вот как можно реализовать декодирование JSON напрямую с объектами Core Data:

Во-первых, вы заставляете свой объект реализовывать Codable. Этот интерфейс должен быть определен в объекте, а не в расширении. Вы также можете определить свои ключи кодирования в этом классе.

class MyManagedObject: NSManagedObject, Codable {
    @NSManaged var property: String?

    enum CodingKeys: String, CodingKey {
       case property = "json_key"
    }
}

Затем вы можете определить метод инициализации. Это также должно быть определено в методе класса, поскольку метод init требуется протоколом Decodable.

required convenience init(from decoder: Decoder) throws {
}

Однако правильный инициализатор для использования с управляемыми объектами:

NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)

Итак, секрет здесь в том, чтобы использовать словарь userInfo для передачи соответствующего объекта контекста в инициализатор. Для этого вам нужно расширить структуру CodingUserInfoKey новым ключом:

extension CodingUserInfoKey {
   static let context = CodingUserInfoKey(rawValue: "context")
}

Теперь вы можете просто в качестве декодера контекста:

required convenience init(from decoder: Decoder) throws {

    guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
    guard let entity = NSEntityDescription.entity(forEntityName: "MyManagedObject", in: context) else { fatalError() }

    self.init(entity: entity, in: context)

    let container = decoder.container(keyedBy: CodingKeys.self)
    self.property = container.decodeIfPresent(String.self, forKey: .property)
}

Теперь, когда вы настраиваете декодирование для управляемых объектов, вам нужно передать соответствующий объект контекста:

let data = //raw json data in Data object
let context = persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
decoder.userInfo[.context] = context

_ = try decoder.decode(MyManagedObject.self, from: data) //we'll get the value from another context using a fetch request later...

try context.save() //make sure to save your data once decoding is complete

Чтобы закодировать данные, вам нужно сделать что-то подобное, используя функцию протокола encode.

person casademora    schedule 24.10.2017
comment
Отличная идея. Есть ли способ таким образом инициализировать, а затем обновить существующие объекты? Например, проверьте, есть ли идентификатор уже в CoreData. Если он существует, загрузите объект и обновите, в противном случае создайте новый (как описано выше). - person 1b0t; 19.12.2017
comment
Спасибо за это, Сол! - person Tom Harrington; 13.01.2018
comment
Я бы добавил, что, поскольку значения декодирования из JSON могут выдавать, этот код потенциально позволяет вставлять объекты в контекст, даже если при декодировании JSON возникла ошибка. Вы можете уловить это из вызывающего кода и обработать его, удалив только что вставленный, но бросающий объект. - person Tom Harrington; 15.01.2018
comment
Привет, я выполнил те же шаги, что и выше, но context.hasChanges всегда дает мне false, даже если управляемый объект имеет значения после декодирования. Поскольку изменений нет, context.save не сохраняется. Я попытался вызвать context.save напрямую, я прошел без ошибок, но база данных пуста. Я также проверил указатель контекста, который был передан декодеру, и он такой же. Любая идея? - person Tarang; 06.02.2018
comment
Я получал Command failed due to signal: Abort trap: 6 ошибку в Xcode9, пока я должным образом не принял Encoder протокол, например: func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(property, forKey: .property) } - person Alexander Stepanishin; 30.03.2018
comment
@Tarang Тебе удалось заставить его работать? У меня такая же проблема, база данных пуста, контекст не сохраняется. - person Akhu; 21.05.2018
comment
@Akhu На самом деле я перешел в базу данных Realm, так как там было много шаблонного кода с CoreData. - person Tarang; 22.05.2018
comment
Этот ответ не поможет, если вы хотите обновить существующий объект. Он всегда создает новый объект и дублирует ваши существующие записи. - person Shamsiddin; 17.06.2018
comment
Есть ли способ сделать что-то подобное, но с использованием структур? - person Sergio Trejo; 24.07.2018
comment
Привет, я кодирую (в приложении iOS) и декодирую (в расширении набора часов), как вам удалось получить там такой же контекст? - person Hemang; 16.10.2018
comment
Здравствуйте, я сначала получаю эти ошибки при создании необязательного свойства внутри класса. Свойство нельзя пометить @NSManaged, потому что его тип не может быть представлен в Objective-C, а не в строке инициализации. Неправильные метки аргументов в вызове (имеют 'entity: in : ', ожидалось' _: с использованием: ') - person Sourav Sachdeva; 26.11.2018
comment
я получаю ошибку времени компиляции "NSManagedObjectContext?" не конвертируется в 'NSManagedObjectContext' на страже let context = decoder.userInfo [.context] as? NSManagedObjectContext else {fatalError ()} - person Ammar Mujeeb; 10.02.2019
comment
Скажем, я не хочу обязательно сохранять данные всякий раз, когда я их декодирую, иногда я просто хочу их декодировать и использовать для других целей, как этого добиться? Потому что, если я не хочу сохранять данные, мне не нужен контекст, и я не буду его передавать, но тогда мой код выйдет из строя, потому что он будет искать контекст в guard и достигнет fatalError(). - person user121095; 19.12.2019
comment
Если вы получаете сообщение об ошибке после реализации этого, в котором говорится, что данные данные не содержат значения верхнего уровня, вам следует попробовать изменить кодогенерацию класса сущности на Вручную / Нет, а затем выбрать Редактор - ›Создать подкласс NSManagedObject и использовать сгенерированный объект class как ваш кодируемый объект - person gyleg5; 18.09.2020

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

Вы по-прежнему можете использовать Codable с ним ограниченными способами, так же как вы можете использовать NSCoding.

Один из способов - декодировать объект (или структуру) с помощью любого из этих протоколов и передать его свойства в новый NSManagedObject экземпляр, который вы создали для документов Core Data.

Другой способ (который очень распространен) - использовать один из протоколов только для нестандартного объекта, который вы хотите сохранить в свойствах управляемого объекта. Под «нестандартным» я подразумеваю все, что не соответствует стандартным типам атрибутов Core Data, указанным в вашей модели. Например, NSColor нельзя сохранить напрямую как свойство управляемого объекта, поскольку это не один из основных типов атрибутов, поддерживаемых CD. Вместо этого вы можете использовать NSKeyedArchiver для сериализации цвета в экземпляр NSData и сохранить его как свойство Data в управляемом объекте. Поверните этот процесс в обратном порядке с помощью NSKeyedUnarchiver. Это упрощенно, и есть гораздо лучший способ сделать это с помощью Core Data (см. Временные атрибуты), но это иллюстрирует мою точку зрения.

Вы также могли бы принять Encodable (один из двух протоколов, составляющих Codable - вы можете угадать имя другого?) Для преобразования экземпляра управляемого объекта непосредственно в JSON для совместного использования, но вам нужно будет укажите ключи кодирования и вашу собственную encode реализацию, поскольку она не будет автоматически синтезироваться компилятором с настраиваемым кодированием ключи. В этом случае вам нужно указать только ключи (свойства), которые вы хотите включить.

Надеюсь это поможет.

person Joshua Nozzi    schedule 09.06.2017
comment
Спасибо за подробное объяснение. В настоящее время я использую первый подход, как вы упомянули. Но я очень надеюсь, что NSManagedObject может соответствовать Codable по умолчанию, и есть такие методы, как json = encoder.encode(foo), для прямого кодирования и foo = decoder.decode(Foo.self, json, context) для прямого декодирования. Надеюсь увидеть это в обновлении или в следующем крупном выпуске. - person hgl; 10.06.2017
comment
Я действительно не стал бы на это рассчитывать. Возможность настраивать кодирование / декодирование в значительной степени охватывает все основы передачи данных между магазином вашего приложения и большинством реальных случаев с помощью только JSON de / coder. Поскольку эти два подхода являются взаимоисключающими для сохранения приложений (из-за их радикально разных подходов к дизайну и вариантов использования), вероятность такой поддержки примерно равна нулю. Но надежда вечна. ;-) - person Joshua Nozzi; 10.06.2017
comment
@JoshuaNozzi Я полностью не согласен с этим комментарием. Вы можете довольно легко изменить сопоставления, и люди используют библиотеки для этого конкретного подхода. Я не удивлюсь, если в будущем поддержка появится в двух или около того итерациях iOS. Это просто потребует настройки протокола для поддержки популяции без инициализации или соответствия базового уровня текущим интерфейсам инициализации CoreData и генерации кода перечисления Codable для моделей CoreData (которые у них уже есть генерация кода). Подходы не исключают друг друга, и 99% приложений, использующих основные данные, отображают JSON. - person TheCodingArt; 15.08.2017
comment
@TheCodingArt Что вы имеете в виду? Пользовательские типы магазинов? Это немного отличается от прямого использования Codable / Decodable непосредственно на отдельных управляемых объектах, кроме механизма Core Data. - person Joshua Nozzi; 15.08.2017
comment
@JoshuaNozzi Я никогда ничего не упоминал о настраиваемых типах хранилищ .... Это простое сопоставление свойств в Swift с сериализацией / десериализацией с генерируемыми кодируемым кодом значениями ключей. - person TheCodingArt; 15.08.2017
comment
@TheCodingArt Хорошо ...? Так что вы говорите? - person Joshua Nozzi; 15.08.2017
comment
@JoshuaNozzi Вы специально отметили, что протокол Codable конфликтует с архитектурой CoreData в вашем комментарии здесь: Поскольку эти два подхода являются взаимоисключающими для сохранения приложения (из-за их радикально разных подходов к дизайну и вариантов использования), вероятность такой поддержки примерно равна нулю. Я считаю, что это ложно. Нет конфликта сериализации и десериализации с моделями CoreData. Это просто отсутствие поддержки генерации кода свойства и текущих требований инициализатора для Decoder. - person TheCodingArt; 15.08.2017
comment
@JoshuaNozzi дает время, чтобы протоколы были приняты NSManagedObject на уровне фреймворка, или чтобы сам протокол изменил поддержку принятия для требований неинициализатора, и это должно быть прямым, чтобы получить эту поддержку на NSManagedObjects. - person TheCodingArt; 15.08.2017
comment
@JoshuaNozzi с изменениями, внесенными в CoreData (согласно последнему обсуждению сеанса WWDC), меня не удивит, если мы получим это как хорошее обновление в ближайшем будущем. Тем не менее, я не думаю, что что-либо подтверждает идею о том, что это конфликтует с какой-либо архитектурой CoreData, и этого не следует ожидать в будущем обновлении. Во всяком случае, я настоятельно рекомендую создать для него радар, чтобы мы увидели его в iOS 12. - person TheCodingArt; 15.08.2017
comment
@TheCodingArt Вы неправильно истолковали мой смысл. Использовать их вместе для хранилища было бы бессмысленно. Очевидно, вы можете добавить поддержку для получения сериализованной версии ваших управляемых объектов для использования в другом месте, но какой смысл в использовании Core Data, если бы вы также использовали Codable для аспекта хранения на уровне управляемого объекта ? - person Joshua Nozzi; 15.08.2017
comment
Давайте продолжим это обсуждение в чате. - person TheCodingArt; 15.08.2017
comment
поскольку он не будет автоматически синтезироваться компилятором с пользовательскими ключами кодирования, почему бы и нет? - person Honey; 19.12.2018
comment
@ Мед Как это могло быть? Автосинтез возможен только потому, что он делает это для всех известных свойств (ключей, кодирования и поведения декодирования). Если вы переопределите какую-либо часть этого (ключи, кодирование или декодирование), это повлияет на некоторые или все другие части, в зависимости от того, соответствуете ли вы кодированию, декодированию или обоим, а также от того, отменяете ли вы автоматическое синтезированные ключи. - person Joshua Nozzi; 20.12.2018
comment
Спасибо 1. Представьте себе: public class CityEntity: NSManagedObject, Codable {}; extension CityEntity { @nonobjc public class func fetchRequest() -> NSFetchRequest<CityEntity> { return NSFetchRequest<CityEntity>(entityName: "CityEntity") } @NSManaged public var name: String? @NSManaged public var population: Int16 };, значит, вы говорите, что ни одна из общедоступных переменных не считается известными свойствами? Ok. Так что это за собственность? Я просто пытаюсь понять свой жаргон. 2. Также вы использовали слово override. Что здесь отменяется? - person Honey; 20.12.2018
comment
@Honey Вам действительно нужно опубликовать это как отдельный вопрос. Тем более, что здесь задействован управляемый объект Core Data, который имеет свои собственные требования и поведение (вдвойне при использовании с Codable). Я признаю, что это сомнительная область для меня, так как я обнаружил, что Core Data ужасно подходит для большей части моей профессиональной работы по разным причинам. Что касается моей терминологии, не зацикливайтесь на ней. Помимо Core Data, компилятор знает, какие свойства вы определяете для классов и структур. Что касается переопределения, я имею в виду, что поведение автосинтезатора компилятора переопределяется в тот момент, когда вы указываете код для своих собственных ключей. - person Joshua Nozzi; 20.12.2018
comment
@Honey Слишком много тонкостей, чтобы было справедливо ожидать какого-либо подробного и информативного ответа в комментариях, поэтому я начал с того, что вы должны опубликовать свою собственную ситуацию как отдельный вопрос, чтобы вы могли точно указать, что это вы пытаетесь достичь / понять именно в вашей ситуации. Это дает людям возможность дать полный ответ без ограничений по количеству символов и с правильным форматированием. Расширенное обсуждение в комментариях не только противоречит использованию SO, но и равносильно написанию приложения с помощью текстового сообщения. ;-) - person Joshua Nozzi; 20.12.2018
comment
Кроме того, мне, как гею, забавно, что ваше имя пользователя заставляет меня звучать так, будто я снисходительно к вам отношусь, называя вас @Honey. «Дорогая, тебе действительно нужно ...» :-D - person Joshua Nozzi; 20.12.2018
comment
Мне очень жаль, ты гей или думаешь, что я гей? :) - person Honey; 20.12.2018
comment
LOL, я имел в виду себя. «Дорогая» - это гейская вещь, которую я стараюсь избегать, если я не шучу. - person Joshua Nozzi; 20.12.2018

Swift 4.2:

Следуя решению касадеморы,

guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError() }

должно быть

guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }.

Это предотвращает ошибки, которые Xcode ошибочно распознает как проблемы среза массива.

Изменить: используйте неявно развернутые опции, чтобы избавиться от необходимости принудительно развертывать .context каждый раз, когда он используется.

person Schemetrical    schedule 24.07.2018
comment
Я бы предпочел сделать статическую константу (.context) force развернутой в определении вместо того, чтобы разбрызгивать ее по всему источнику, как это. - person casademora; 26.09.2018
comment
@casademora, это то же самое, что и ваш ответ, только для быстрого 4.2 (РЕДАКТИРОВАТЬ: я понимаю, что вы имеете в виду. Неявно развернутые опции :-). ). - person Schemetrical; 27.09.2018
comment
Да, я понимаю разницу. Я просто предлагаю развернуть константу (в одном месте) в отличие от метода доступа userInfo (потенциально везде) - person casademora; 27.09.2018
comment
Привет, я кодирую (в приложении iOS) и декодирую (в расширении набора часов), как вам удалось получить там такой же контекст? - person Hemang; 16.10.2018

В качестве альтернативы для тех, кто хотел бы использовать современный подход XCode к созданию NSManagedObject файлов, я создал класс DecoderWrapper для предоставления объекта Decoder, который затем использую в своем объекте, который соответствует протоколу JSONDecoding:

class DecoderWrapper: Decodable {

    let decoder:Decoder

    required init(from decoder:Decoder) throws {

        self.decoder = decoder
    }
}

protocol JSONDecoding {
     func decodeWith(_ decoder: Decoder) throws
}

extension JSONDecoding where Self:NSManagedObject {

    func decode(json:[String:Any]) throws {

        let data = try JSONSerialization.data(withJSONObject: json, options: [])
        let wrapper = try JSONDecoder().decode(DecoderWrapper.self, from: data)
        try decodeWith(wrapper.decoder)
    }
}

extension MyCoreDataClass: JSONDecoding {

    enum CodingKeys: String, CodingKey {
        case name // For example
    }

    func decodeWith(_ decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try container.decode(String.self, forKey: .name)
    }
}

Это, вероятно, полезно только для моделей без каких-либо необязательных атрибутов, но это решает мою проблему, заключающуюся в том, что я хочу использовать Decodable, но также управлять отношениями и постоянством с помощью Core Data без необходимости вручную создавать все мои классы / свойства.

Изменить: пример использования

Если у меня есть объект json:

let myjson = [ "name" : "Something" ]

Я создаю объект в Core Data (здесь для краткости приведено принудительное приведение):

let myObject = NSEntityDescription.insertNewObject(forEntityName: "MyCoreDataClass", into: myContext) as! MyCoreDataClass

И я использую расширение, чтобы объект декодировал json:

do {
    try myObject.decode(json: myjson)
}
catch {
    // handle any error
}

Сейчас myObject.name это "Something"

person ChrisH    schedule 05.02.2019
comment
Если у нас есть настраиваемый объект, например, @NSManaged public var products: NSSet ?. Как мы будем декодировать этот объект. - person user1770342; 02.12.2019
comment
Вы можете преобразовать его в обычный набор, который можно кодировать - person devjme; 11.01.2021