Swift: Insert codable object into Core Data - swift

I'm getting a response from an API and decoding the response like this:
struct MyStuff: Codable {
let name: String
let quantity: Int
let location: String
}
And I have instance an Entity to map MyStuff:
#objc(Stuff)
public class Stuff: NSManagedObject {
}
extension Stuff {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Stuff> {
return NSFetchRequest<Stuff>(entityName: "Stuff")
}
#NSManaged public var name: String?
#NSManaged public var quantity: Int64
#NSManaged public var location: String?
}
My question is, when I have the response of type MyStuff there is a way to loop thru the keys and map the values to core data?
for example:
let myStuff = MyStuff(name: "table", quantity: 1, location: "kitchen")
let myStuff = MyStuff(name: "table", quantity: 1, location: "kitchen")
for chidren in Mirror(reflecting: myStuff).children {
print(chidren.label)
print(chidren.value)
/*
insert values to core data
*/
}
I'll really appreciate your help

A smart solution is to adopt Decodable in Stuff
Write an extension of CodingUserInfoKey and JSONDecoder
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")!
}
extension JSONDecoder {
convenience init(context: NSManagedObjectContext) {
self.init()
self.userInfo[.context] = context
}
}
In Stuff adopt Decodable and implement init(from:), it must be implemented in the class, not in the extension
#objc(Stuff)
public class Stuff: NSManagedObject, Decodable {
private enum CodingKeys: String, CodingKey { case name, quantity, location }
public required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("Error: context doesn't exist!") }
let entity = NSEntityDescription.entity(forEntityName: "Stuff", in: context)!
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
quantity = try values.decodeIfPresent(Int64.self, forKey: .quantity) ?? 0
location = try values.decodeIfPresent(String.self, forKey: .location)
}
}
To decode the JSON you have to initialize the decoder with the convenience initializer
let decoder = JSONDecoder(context: context)
where context is the current NSManagedObjectContext instance.
Now you can create Stuff instances directly from the JSON.

You can store entire object as JSONString if you don't support query for each field.
If you need query for some field then keep that field in entity object.
struct MyStuff: Codable {
let name: String
let quantity: Int
let location: String
}
extension Encodable {
func toString() -> String? {
if let config = try? JSONEncoder().encode(self) {
return String(data: config, encoding: .utf8)
}
return .none
}
}
extension Decodable {
static func map(JSONString: String) -> Self? {
try? JSONDecoder().decode(Self.self, from: JSONString.data(using: .utf8) ?? .init())
}
}
#objc(Stuff)
public class Stuff: NSManagedObject {
}
// Entity with single field (no field base query support)
extension Stuff {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Stuff> {
return NSFetchRequest<Stuff>(entityName: "Stuff")
}
#NSManaged public var myStuffRawJSON: String?
func mapToMyStuff() -> MyStuff? {
MyStuff.map(JSONString: myStuffRawJSON ?? "")
}
}
How to use:
let myStuff = MyStuff(name: "table", quantity: 1, location: "kitchen")
let entity: Stuff //Create entity
entity.myStuffRawJSON = myStuff.toString()
// save your entity

Related

Cannot map protocol compliant elements to generic elements

As you can see below, I downloaded an array of structures containing heterogeneous objects that were decoded into enums containing nested objects.
I would now like to put said objects into a generic Model structure, but the compiler won't allow this - the error is described below in the code comment. I am relatively new to programming in Swift, I would appreciate your help.
import Foundation
let jsonString = """
{
"data":[
{
"type":"league",
"info":{
"name":"NBA",
"sport":"Basketball",
"website":"https://nba.com/"
}
},
{
"type":"player",
"info":{
"name":"Kawhi Leonard",
"position":"Small Forward",
"picture":"https://i.ibb.co/b5sGk6L/40a233a203be2a30e6d50501a73d3a0a8ccc131fv2-128.jpg"
}
},
{
"type":"team",
"info":{
"name":"Los Angeles Clippers",
"state":"California",
"logo":"https://logos-download.com/wp-content/uploads/2016/04/LA_Clippers_logo_logotype_emblem.png"
}
}
]
}
"""
struct Response: Decodable {
let data: [Datum]
}
struct League: Codable {
let name: String
let sport: String
let website: URL
}
extension League: Displayable {
var text: String { name }
var image: URL { website }
}
struct Player: Codable {
let name: String
let position: String
let picture: URL
}
extension Player: Displayable {
var text: String { name }
var image: URL { picture }
}
struct Team: Codable {
let name: String
let state: String
let logo: URL
}
extension Team: Displayable {
var text: String { name }
var image: URL { logo }
}
enum Datum: Decodable {
case league(League)
case player(Player)
case team(Team)
enum DatumType: String, Decodable {
case league
case player
case team
}
private enum CodingKeys : String, CodingKey { case type, info }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DatumType.self, forKey: .type)
switch type {
case .league:
let item = try container.decode(League.self, forKey: .info)
self = .league(item)
case .player:
let item = try container.decode(Player.self, forKey: .info)
self = .player(item)
case .team:
let item = try container.decode(Team.self, forKey: .info)
self = .team(item)
}
}
}
protocol Displayable {
var text: String { get }
var image: URL { get }
}
struct Model<T: Displayable> {
let text: String
let image: URL
init(item: T) {
self.text = item.text
self.image = item.image
}
}
do {
let response = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
let items = response.data
let models = items.map { (item) -> Model<Displayable> in // error: only struct/enum/class types can conform to protocols
switch item {
case .league(let league):
return Model(item: league)
case .player(let player):
return Model(item: player)
case .team(let team):
return Model(item: team)
}
}
} catch {
print(error)
}
You do not need generics here.
Change Model to accept any type that conforms to Displayable in the init
struct Model {
let text: String
let image: URL
init(item: Displayable) {
self.text = item.text
self.image = item.image
}
}
and then change the closure to return Model
let models = items.map { (item) -> Model in
If you want to keep your Model struct generic then you need to change the map call to
let models: [Any] = items.map { item -> Any in
switch item {
case .league(let league):
return Model(item: league)
case .player(let player):
return Model(item: player)
case .team(let team):
return Model(item: team)
}
}
This will give the following output when conforming to CustomStringConvertible
extension Model: CustomStringConvertible {
var description: String {
"\(text) type:\(type(of: self))"
}
}
print(models)
[NBA type:Model<League>, Kawhi Leonard type:Model<Player>, Los Angeles Clippers type:Model<Team>]
If you are only interested in name and the key representing the URL parse the JSON directly into Model by using nestedContainer this way
struct Response: Decodable {
let data: [Model]
}
struct Model: Decodable {
let name: String
let image: URL
enum DatumType: String, Decodable {
case league
case player
case team
}
private enum CodingKeys : String, CodingKey { case type, info }
private enum CodingSubKeys : String, CodingKey { case name, website, picture, logo }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DatumType.self, forKey: .type)
let subContainer = try container.nestedContainer(keyedBy: CodingSubKeys.self, forKey: .info)
self.name = try subContainer.decode(String.self, forKey: .name)
let urlKey : CodingSubKeys
switch type {
case .league: urlKey = .website
case .player: urlKey = .picture
case .team: urlKey = .logo
}
self.image = try subContainer.decode(URL.self, forKey: urlKey)
}
}
do {
let response = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
let data = response.data
print(data)
} catch {
print(error)
}

Create a generic Data initializer for Decodable types using Swift

#objcMembers
public class MyResponse: NSObject, Codable {
public let id: String?
public let context: String?
public let results: [MyResult]?
}
What is the proper way to parse MyResponse from Data in class extension?
I tried the following, but got error "Cannot assign to value: 'self' is immutable.
Cannot assign value of type 'MyResponse' to type 'Self'."
extension MyResponse {
public convenience init(data: Data) throws {
self = try JSONDecoder().decode(MyResponse.self, from: data)
}
}
You can extend Decodable protocol and create a generic initializer:
extension Decodable {
public init(data: Data, using decoder: JSONDecoder = JSONDecoder()) throws {
self = try decoder.decode(Self.self, from: data)
}
}
You can't overwrite class itself, but you can init it, init object from json and then assign values/ If take your code - it'll be something like this:
public class MyResponse: Codable {
public var id: String?
public var context: String?
public var results: [MyResult]?
}
public extension MyResponse {
convenience init(data: Data) throws {
self.init()
let object = try JSONDecoder().decode(MyResponse.self, from: data)
self.id = object.id
self.context = object.context
self.results = object.results
}
}
If you really don't need a class it's better to use struct instead of it, and it can be like this:
public struct MyResponse: Codable {
public let id: String?
public let context: String?
public let results: [String]?
}
public extension MyResponse {
init(data: Data) throws {
self = try JSONDecoder().decode(MyResponse.self, from: data)
}
}

Save to CoreData array of strings with Codable

I'm stuck.
I have json (array of Movies). I'm trying parse it with Codable protocol, and save to Core Data.
Problem is that Movie object have array of Genres (array of strings). I created two entities: Movie and Genre (with relation One to Many). Parsing Movie object not have problem, but when I try to parse genres - its not working.
Have any idea?
P.S. Yes I know that genre array not have key "name".
{
"title": "Dawn of the Planet of the Apes",
"image": "https://api.androidhive.info/json/movies/1.jpg",
"rating": 8.3,
"releaseYear": 2014,
"genre": ["Action", "Drama", "Sci-Fi"]
},
{
"title": "District 9",
"image": "https://api.androidhive.info/json/movies/2.jpg",
"rating": 8,
"releaseYear": 2009,
"genre": ["Action", "Sci-Fi", "Thriller"]
}
Movie model:
#objc(Movie)
class Movie: NSManagedObject, Decodable {
#NSManaged var title: String?
#NSManaged var image: String?
#NSManaged var rating: Float
#NSManaged var releaseYear: Int
#NSManaged var genres: Set<Genre>?
enum apiKey: String, CodingKey {
case title
case image
case rating
case releaseYear
case genres = "genre"
}
#nonobjc public class func request() -> NSFetchRequest<Movie> {
return NSFetchRequest<Movie>(entityName: "Movie")
}
// MARK: - Decodable
public required convenience init(from decoder: Decoder) throws {
guard let contextUserInfoKey = CodingUserInfoKey.context,
let manageObjContext = decoder.userInfo[contextUserInfoKey] as? NSManagedObjectContext,
let manageObjMovie = NSEntityDescription.entity(forEntityName: "Movie", in: manageObjContext) else {
fatalError("Error to getting context")
}
self.init(entity: manageObjMovie, insertInto: manageObjContext)
let container = try decoder.container(keyedBy: apiKey.self)
self.title = try container.decodeIfPresent(String.self, forKey: .title)
self.image = try container.decodeIfPresent(String.self, forKey: .image)
self.rating = try container.decodeIfPresent(Float.self, forKey: .rating) ?? 0
self.releaseYear = try container.decodeIfPresent(Int.self, forKey: .releaseYear) ?? 0
self.genres = try container.decodeIfPresent(Set<Genre>.self, forKey: .genres) ?? []
}
}
// MARK: Generated accessors for geonames
extension Movie {
#objc(addGenresObject:)
#NSManaged func addToGenres(_ value: Genre)
#objc(setKeyObject:)
#NSManaged func setKeyObject(_ value: String)
}
Genre model:
#objc(Genre)
class Genre: NSManagedObject, Decodable {
#NSManaged var name: String?
enum apiKey: String, CodingKey {
case name
}
#nonobjc public class func request() -> NSFetchRequest<Genre> {
return NSFetchRequest<Genre>(entityName: "Genre")
}
// MARK: - Decodable
public required convenience init(from decoder: Decoder) throws {
guard let contextUserInfoKey = CodingUserInfoKey.context,
let manageObjContext = decoder.userInfo[contextUserInfoKey] as? NSManagedObjectContext,
let manageObjGenre = NSEntityDescription.entity(forEntityName: "Genre", in: manageObjContext) else {
fatalError("Error to getting context")
}
self.init(entity: manageObjGenre, insertInto: manageObjContext)
let container = try decoder.container(keyedBy: apiKey.self)
self.name = try container.decodeIfPresent(String.self, forKey: .name)
}
}
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.

Issue using CoreData with Codable protocol

I created two NSManagedObject classes one for Songs and one for Categories of each song. And they have a one to many relationship. What i do is that i download a json file from the server and parse it using Decodable and save the data in CoreData. Every thing is smooth except when i try to add songs to a certain category type i get a crash.
'Illegal attempt to establish a relationship 'category' between objects in different contexts
I know what this crash is and i know i have two context one for the category class and one for the song class. The issue is that tutorials for CoreData using Decodable is so little. So now i am thinking of a way maybe i can create a parent class of these classes and init the context in it and just call super.init() in the subclasses of category and songs. But i really cannot do it. Or maybe there is a much simpler way. I will share the code of my classes here and the code where the error is happening.
struct CategoryData: Decodable {
let data: [CategoryManagedObject]
}
#objc(CategoryManagedObject)
class CategoryManagedObject: NSManagedObject, Decodable {
// MARK: - Core Data Managed Object
#NSManaged var id: Int
#NSManaged var name: String
#NSManaged var imgUrl: String
#NSManaged var coverPhotoBit64: String
#NSManaged var jsonUrl: String
#NSManaged var version: Int
#NSManaged var order: Int
#NSManaged var songs: NSSet?
//var coreDataStack: CoreDataManager!
enum CodingKeys: String, CodingKey {
case name, coverPhotoBit64, id, jsonUrl, version, order
case imgUrl = "coverPhoto"
}
// MARK: - Decodable
required convenience init(from decoder: Decoder) throws {
//try super.init(from: decoder, type: "Categories")
guard let codingUserInfoKeyManagedObjectContext = CodingUserInfoKey.context,
let managedObjectContext = decoder.userInfo[codingUserInfoKeyManagedObjectContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "Categories", in: managedObjectContext) else {
fatalError("FALIED TO DECODE CATEGORIES")
}
self.init(entity: entity, insertInto: managedObjectContext)
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
imgUrl = try container.decode(String.self, forKey: .imgUrl)
coverPhotoBit64 = try container.decode(String.self, forKey: .coverPhotoBit64)
version = try container.decode(Int.self, forKey: .version)
jsonUrl = try container.decode(String.self, forKey: .jsonUrl)
order = try container.decode(Int.self, forKey: .order)
// if let sArray = songs.allObjects as? [Song] {
// songs = try container.decode(sArray.self, forKey: .song)
// }
}
#nonobjc public class func fetchRequest() -> NSFetchRequest<CategoryManagedObject> {
return NSFetchRequest<CategoryManagedObject>(entityName: "Categories")
}
}
public extension CodingUserInfoKey {
// Helper property to retrieve the context
static let context = CodingUserInfoKey(rawValue: "managedObjectContext")
}
// MARK: Generated accessors for songs
extension CategoryManagedObject {
#objc(addSongsObject:)
#NSManaged public func addToSongs(_ value: Song)
#objc(removeSongsObject:)
#NSManaged public func removeFromSongs(_ value: Song)
#objc(addSongs:)
#NSManaged public func addToSongs(_ values: NSSet)
#objc(removeSongs:)
#NSManaged public func removeFromSongs(_ values: NSSet)
}
#objc(Song)
public class Song: NSManagedObject, Decodable {
#NSManaged var id: Int
#NSManaged var name: String
#NSManaged var artist: String
#NSManaged var code: String
#NSManaged var category: CategoryManagedObject
enum CodingKeys: String, CodingKey {
case name, id, artist, code
}
// MARK: - Decodable
required convenience public init(from decoder: Decoder) throws {
guard let codingUserInfoKeyManagedObjectContext = CodingUserInfoKey.context,
let managedObjectContext = decoder.userInfo[codingUserInfoKeyManagedObjectContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "Songs", in: managedObjectContext) else {
fatalError("FALIED TO DECODE CATEGORIES")
}
self.init(entity: entity, insertInto: managedObjectContext)
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
artist = try container.decode(String.self, forKey: .artist)
code = try container.decode(String.self, forKey: .code)
}
#nonobjc public class func fetchRequest() -> NSFetchRequest<Song> {
return NSFetchRequest<Song>(entityName: "Songs")
}
}
This is where the crash happens because of two different context.
func saveJsonSongsInDB(filename fileName: String, category: CategoryManagedObject) {
do {
let data = try Data(contentsOf: URL(string: fileName)!)
//let context = CoreDataManager.shared.persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
decoder.userInfo[CodingUserInfoKey.context!] = dbContext
//decoder.userInfo[CodingUserInfoKey.deferInsertion] = true
coreDataStack.deleteAllRecords("Songs")
let songs = try decoder.decode([Song].self, from: data)
let s = NSSet(array: songs)
// category.managedObjectContext?.insert(<#T##object: NSManagedObject##NSManagedObject#>)
// dbContext.insert(category)
//print("SONGS: \(songs)")
category.addToSongs(s) //----> CRASH
try dbContext.save()
} catch let err {
print("error:\(err)")
}
}
First of all use one context, the context passed in the JSONDEcoder
In CategoryManagedObject declare songs as non-optional native type
#NSManaged var songs: Set<Song>
Decode songs as Set (yes, this is possible) and set the category of each song to self
songs = try container.decode(Set<Song>.self, forKey: .song)
songs.forEach{ $0.category = self }
That's all. You don't have to set the inverse relationship in CategoryManagedObject
To insert the data you have to decode [CategoryManagedObject]
let decoder = JSONDecoder()
decoder.userInfo[CodingUserInfoKey.context!] = dbContext
coreDataStack.deleteAllRecords("Songs")
_ = try decoder.decode([CategoryManagedObject].self, from: data)

How to encode Realm's List<> type

I am trying to encode my Realm database to JSON. Everything is working except the List<> encoding. So my question is, how would you encode List<>? Because the List doesn't conform to Encodable neighter Decodable protocol.
Right now I am doing this:
#objcMembers class User: Object, Codable{
dynamic var name: String = ""
let dogs = List<Dog>()
private enum UserCodingKeys: String, CodingKey {
case name
case dogs
}
convenience init(name: String) {
self.init()
self.name = name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: UserCodingKeys.self)
try container.encode(name, forKey: .name)
}
#objcMembers class Dog: Object, Codable{
dynamic var name: String = ""
dynamic var user: User? = nil
private enum DogCodingKeys: String, CodingKey {
case name
}
convenience init(name: String) {
self.init()
name = name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DogCodingKeys.self)
try container.encode(name, forKey: .name)
}
}
and like this I am trying to do it:
var json: Any?
let user = RealmService.shared.getUsers()
var usersArray = [User]()
for user in users{
usersArray.append(user)
}
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
let encodedJson = try? jsonEncoder.encode(portfoliosArray)
if let data = encodedJson {
json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
if let json = json {
print(String(describing: json))
}
}
So the question is how I am able to encode the List<Dog>?
To make a Realm object model class with a property of type List conform to Encodable, you can simply convert the List to an Array in the encode(to:) method, which can be encoded automatically.
extension User: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.username, forKey: .username)
let dogsArray = Array(self.dogs)
try container.encode(dogsArray, forKey: .dogs)
}
}
Test classes I used (slightly different from the ones in your question, but I already had these on hand and the methods in question will be almost identical regardless of the variable names):
class Dog: Object,Codable {
#objc dynamic var id:Int = 0
#objc dynamic var name:String = ""
}
class User: Object, Decodable {
#objc dynamic var id:Int = 0
#objc dynamic var username:String = ""
#objc dynamic var email:String = ""
let dogs = List<Dog>()
private enum CodingKeys: String, CodingKey {
case id, username, email, dogs
}
required convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
username = try container.decode(String.self, forKey: .username)
email = try container.decode(String.self, forKey: .email)
let dogsArray = try container.decode([Dog].self, forKey: .dogs)
dogs.append(objectsIn: dogsArray)
}
}
Test the encoding/decoding:
let userJSON = """
{
"id":1,
"username":"John",
"email":"example#ex.com",
"dogs":[
{"id":2,"name":"King"},
{"id":3,"name":"Kong"}
]
}
"""
do {
let decodedUser = try JSONDecoder().decode(User.self, from: userJSON.data(using: .utf8)!)
let encodedUser = try JSONEncoder().encode(decodedUser)
print(String(data: encodedUser, encoding: .utf8)!)
} catch {
print(error)
}
Output:
{"username":"John","dogs":[{"id":2,"name":"King"},{"id":3,"name":"Kong"}]}
You could resort to a mini-hack by making List conform to Encodable:
extension List: Encodable {
public func encode(to coder: Encoder) throws {
// by default List is not encodable, throw an exception
throw NSError(domain: "SomeDomain", code: -1, userInfo: nil)
}
}
// let's ask it to nicely encode when Element is Encodable
extension List where Element: Encodable {
public func encode(to coder: Encoder) throws {
var container = coder.unkeyedContainer()
try container.encode(contentsOf: self)
}
}
Two extensions are needed as you can't add protocol conformance and where clauses at the same time.
Also note that this approach doesn't provide compile-time checks - e.g. a List<Cat> will throw an exception an runtime if Cat is not encodable, instead of a nice compile time error.
The upside is lot of boilerplate code no longer needed:
#objcMembers class User: Object, Encodable {
dynamic var name: String = ""
let dogs = List<Dog>()
convenience init(name: String) {
self.init()
self.name = name
}
}
#objcMembers class Dog: Object, Encodable {
dynamic var name: String = ""
dynamic var user: User? = nil
convenience init(name: String) {
self.init()
name = name
}
}
This is also scalable, as adding new classes don't require any encoding code, but with the mentioned downside of not being fully type safe at compile time.