Save complex JSON to Core Data in Swift
It's quite easy to decode the JSON directly into Core Data with Decodable
First of all create extensions of
CodingUserInfoKey
andJSONDecoder
to be able to pass the managed object contextextension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")!
}
extension JSONDecoder {
convenience init(context: NSManagedObjectContext) {
self.init()
self.userInfo[.context] = context
}
}Add conformance to
Decodable
in both classesclass Name: NSManagedObject, Decodable {
class ParamDownload: NSManagedObject, Decodable {In the class
Name
(not the extension) addprivate enum CodingKeys: String, CodingKey { case name, iin, isOn }
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("NSManagedObjectContext is missing") }
let entity = NSEntityDescription.entity(forEntityName: "Name", in: context)!
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
iin = try values.decode(String.self.self, forKey: .iin)
isOn = try values.decode(Bool.self.self, forKey: .isOn)
}In the class
ParamDownload
(not the extension) addprivate enum CodingKeys: String, CodingKey { case code = "Code", desc = "Desc", names = "iinData" }
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("NSManagedObjectContext is missing") }
let entity = NSEntityDescription.entity(forEntityName: "ParamDownload", in: context)!
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
code = try values.decode(String.self, forKey: .code)
desc = try values.decode(String.self, forKey: .desc)
names = try values.decode(Set<Name>.self, forKey: .names)
names.forEach { $0.pd = self }
}
To decode the JSON create the Decoder with the convenience initializer
let decoder = JSONDecoder(context: CoreDataStack.sharedInstance.persistentContainer.viewContext)
the init
methods handle the relationships.
I recommend to declare the Core Data attributes as much non-optional as possible.
If an attribute must be remain optional replace decode
with decodeIfPresent
.
Decoding JSON nested dictionary using Decodable and storing it using Core Data
It's not necessary that SubCurrencyRate
conforms to Decodable
but you have to declare the reverse relationship (also in the data model!).
class SubCurrencyRate: NSManagedObject {
@NSManaged public var currency: String
@NSManaged public var rate: Double
@NSManaged public var currencyRate: CurrencyRate
To be able to parse JSON directly in Core Data declare two extensions
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")!
}
extension JSONDecoder {
convenience init(context: NSManagedObjectContext) {
self.init()
self.userInfo[.context] = context
}
}
In CurrencyRate
you have to declare the to-many relationship as Set<SubCurrencyRate>
, in init(from decoder
decode the rates dictionary and map it to SubCurrencyRate
instances. If the relationships are defined correctly in the model then the property as well as the reverse relationship is populated automatically.
class CurrencyRate: NSManagedObject, Decodable {
enum CodingKeys: String, CodingKey { case base, date, rates }
@NSManaged public var base: String
@NSManaged public var date: Date
@NSManaged public var rates: Set<SubCurrencyRate>
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("Error: No object context!") }
let entity = NSEntityDescription.entity(forEntityName: "CurrencyRate", in: context)!
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
base = try values.decode(String.self, forKey: .base)
date = try values.decode(Date.self, forKey: .date)
let ratesData = try values.decode([String:Double].self, forKey: .rates)
for (key, value) in ratesData {
let subCurrencyRate = SubCurrencyRate(context: context)
subCurrencyRate.currency = key
subCurrencyRate.rate = value
subCurrencyRate.currencyRate = self
}
}
}
To decode the JSON you have to use the JSONDecoder(context:
API in the extension passing the managed object context and to decode date
properly you have to add an appropriate dateDecodingStrategy
.
Save to CoreData array of strings with Codable
You need an inverse relationship to Movie
in Genre
. Add this
@NSManaged var movie: Movie?
and establish the connection in the model file.
Then decode an array of strings, map it to Genre instances and assign self
to that relationship at the end of the init method
let genreData = try container.decodeIfPresent([String].self, forKey: .genres) ?? []
let genreArray = genreData.map { name in
let genre = Genre(context: manageObjContext)
genre.name = name
genre.movie = self
return genre
}
self.genres = Set(genreArray)
Consider to use a to-many relationship from Genre
to Movie
as well because otherwise you will have a lot of Genre
instances with the same name. And consider also to reduce the optionals in the Core Data classes. It seems that the JSON source provides always all fields. You can get rid of a lot of redundant code.
How to use swift 4 Codable in Core Data?
You can use the Codable interface with CoreData objects to encode and decode data, however it's not as automatic as when used with plain old swift objects. Here's how you can implement JSON Decoding directly with Core Data objects:
First, you make your object implement Codable. This interface must be defined on the object, and not in an extension. You can also define your Coding Keys in this class.
class MyManagedObject: NSManagedObject, Codable {
@NSManaged var property: String?
enum CodingKeys: String, CodingKey {
case property = "json_key"
}
}
Next, you can define the init method. This must also be defined in the class method because the init method is required by the Decodable protocol.
required convenience init(from decoder: Decoder) throws {
}
However, the proper initializer for use with managed objects is:
NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)
So, the secret here is to use the userInfo dictionary to pass in the proper context object into the initializer. To do this, you'll need to extend the CodingUserInfoKey
struct with a new key:
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")
}
Now, you can just as the decoder for the 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)
}
Now, when you set up the decoding for Managed Objects, you'll need to pass along the proper context object:
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
To encode data, you'll need to do something similar using the encode protocol function.
Related Topics
Change Title of a Navigation Bar Button Item
iOS Swift - Custom Camera Overlay
Supporting a Nsmanagedobject Fetchrequest() Class Method in iOS 9 and 10
How to Execute a Function in a Uiviewcontroller Through a Swiftui Button
Convenience Initialization of Uinavigationcontroller Subclass Makes Subclass Constant Init Twice
My Uiviews Muck-Up When I Combine Uipangesturerecognizer and Autolayout
Fill Uiview with Color Based on Percentage
How to Set Kerning (Spacing Between Characters) on Uinavigationbar Title - Swift or Objective-C
Get Center Coordinates from Mapkit and Display in Uilabel
Cocoa Singleton and Shared Instances
Why the Autolayout Is Not Updated If I Use Ibdesignable File When I Run the App
What am I Doing Wrong with Allowsfractionalunits on Nsdatecomponentsformatter
Hidden Property Cannot Be Changed Within an Animation Block
Cannot Invoke Initializer for Type Unsafemutablepointer<Uint8>
Swiftui Transition from Modal Sheet to Regular View with Navigation Link