Save to CoreData array of strings with Codable - swift

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.

Related

Swift: Insert codable object into Core Data

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

SwiftUI ObservableObject create non array #Published

I tried to create an ObservableObject with non array #Published item. However, I still don't know how to do so. I tried to use a ? to do so. But when I display it in view like Text((details.info?.name)!), and it return Thread 1: Swift runtime failure: force unwrapped a nil value I don't know what the problem and how to solve. Is it my method of creating observable object class are correct?
class ShopDetailJSON: ObservableObject {
#Published var info : Info?
init(){load()}
func load() {
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print("No data in response: \(error?.localizedDescription ?? "Unknown error").")
return
}
if let decodedShopDetails = try? JSONDecoder().decode(ShopDetail.self, from: data) {
DispatchQueue.main.async {
self.info = decodedShopDetails.info!
}
} else {
print("Invalid response from server")
}
}.resume()
}
}
struct Info : Codable, Identifiable {
let contact : String?
let name : String?
var id = UUID()
enum CodingKeys: String, CodingKey {
case contact = "contact"
case name = "name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
contact = try values.decodeIfPresent(String.self, forKey: .contact)
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
struct ShopDetail : Codable {
let gallery : [Gallery]?
let info : Info?
enum CodingKeys: String, CodingKey {
case gallery = "gallery"
case info = "info"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
gallery = try values.decodeIfPresent([Gallery].self, forKey: .gallery)
info = try values.decodeIfPresent(Info.self, forKey: .info)
}
}
Sample JSON data
{
"gallery": [],
"info": {
"contact": "6012345678",
"name": "My Salon",
}
}
This is answer is a bit of a guess as to what happens in your code, but if the JSON data is never null, as you say in the comments, it's likely that you're trying to access a not-yet-updated ShopDetailJSON.info optional property in your view.
First, some clean-up. You don't need to the custom implementation of init(from:) - just conforming to Codable is enough in your case. And if the JSON values aren't optional, no need to make them into an optional type:
struct Info: Codable, Identifiable {
let contact : String
let name : String
var id = UUID()
}
struct ShopDetail: Codable {
let gallery : [Gallery]
let info : Info
}
Then, when you get the JSON you wouldn't need to deal with optionals and force-unwrap ! (which should have been avoided anyways):
if let decodedShopDetails = try? JSONDecoder().decode(ShopDetail.self, from: data {
DispatchQueue.main.async {
self.info = decodedShopDetails.info // no force-unwrap needed
}
}
In the view, you need to check that the info property is not nil before accessing its elements.
struct ContentView: View {
#ObservedObject var details: ShopDetailJSON
var body: some View {
Group() {
// on iOS14
if let info = details.info {
Text(info.name)
}
// pre iOS14
// if details.info != nil {
// Text(details.info!.name)
// }
}
.onAppear {
self.details.load()
}
}
}

Why does my Core Data Model Save Error Code=1570 show?

Inside my CoreDataManager class, I'm trying to save my CurrentWeather properties by setting the value.
Then inside the do-catch block it tries to save the context, then skips to the error block and prints out the error instead. I'm wondering what steps I did wrong here.
I have two classes subclassing NSManagedObject, first WeatherWrapper then CurrentWeather.
func addCurrentWeather(weather: CurrentWeather) -> CurrentWeather? {
let entity = NSEntityDescription.entity(forEntityName: "CurrentWeather", in: context)!
let currentWeather = NSManagedObject(entity: entity, insertInto: context)
currentWeather.setValue(weather.temperature, forKeyPath: "temperature")
currentWeather.setValue(weather.summary, forKeyPath: "summary")
currentWeather.setValue(weather.time, forKeyPath: "time")
do {
try context.save()
return currentWeather as? CurrentWeather
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
return nil
}
}
Here is the debug console.
Here is WeatherWrapper Model inside it having a property type CurrentWeather
#objc(WeatherWrapper)
public class WeatherWrapper: NSManagedObject, Codable {
#NSManaged public var latitude: Double
#NSManaged public var longitude: Double
#NSManaged public var timezone: String
#NSManaged public var currentWeather: CurrentWeather
enum CodingKeys: String, CodingKey {
case latitude
case longitude
case timezone
case currentWeather = "currently"
}
public required convenience init(from decoder: Decoder) throws {
guard
let contextUserInfoKey = CodingUserInfoKey.context,
let managedObjectContext = decoder.userInfo[contextUserInfoKey] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "WeatherWrapper", in: managedObjectContext) else {
fatalError("Could not retrieve context")
}
self.init(entity: entity, insertInto: managedObjectContext)
let container = try decoder.container(keyedBy: CodingKeys.self)
latitude = try container.decode(Double.self, forKey: .latitude)
longitude = try container.decode(Double.self, forKey: .longitude)
timezone = try container.decode(String.self, forKey: .timezone)
currentWeather = try container.decode(CurrentWeather.self, forKey: .currentWeather)
}
}
Here is my CurrentWeather Model, but why have weatherWrapper property? Doesn't make sense to me.
#objc(CurrentWeather)
public class CurrentWeather: NSManagedObject, Codable {
#NSManaged public var time: Int32
#NSManaged public var summary: String
#NSManaged public var temperature: Double
// #NSManaged public var weatherWrapper: WeatherWrapper
enum CodingKeys: String, CodingKey {
case time
case summary
case temperature
// case weatherWrapper
}
required convenience public init(from decoder: Decoder) throws {
guard
let contextUserInfoKey = CodingUserInfoKey(rawValue: "context"),
let managedObjectContext = decoder.userInfo[contextUserInfoKey] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "CurrentWeather", in: managedObjectContext) else {
fatalError("Could not retrieve context")
}
self.init(entity: entity, insertInto: managedObjectContext)
let values = try decoder.container(keyedBy: CodingKeys.self)
time = try values.decode(Int32.self, forKey: .time)
summary = try values.decode(String.self, forKey: .summary)
temperature = try values.decode(Double.self, forKey: .temperature)
// weatherWrapper = try values.decode(WeatherWrapper.self, forKey: .weatherWrapper)
}
}
It seems you have to set
currentWeather.setValue(<#somevalue#>, forKeyPath: "weatherWrapper")

Failable Initializers with Codable

I'm attempting to parse the following json schema of array of items, itemID may not be empty. How do I make an item nil id itemID does not exist in the JSON?
[{
"itemID": "123",
"itemTitle": "Hello"
},
{},
...
]
My decodable classes are as follows:
public struct Item: : NSObject, Codable {
let itemID: String
let itemTitle: String?
}
private enum CodingKeys: String, CodingKey {
case itemID
case itemTitle
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
itemID = try container.decode(String.self, forKey: .itemID)
itemTitle = try container.decodeIfPresent(String.self, forKey: .itemTitle)
super.init()
}
}
First of all, itemID is an Int and not String in your JSON response. So the struct Item looks like,
public struct Item: Codable {
let itemID: Int?
let itemTitle: String?
}
Parse the JSON like,
if let data = data {
do {
let items = try JSONDecoder().decode([Item].self, from: data).filter({$0.itemID == nil})
print(items)
} catch {
print(error)
}
}
In the above code you can simply filter out the items with itemID == nil.

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)