Issue using CoreData with Codable protocol - swift

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)

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

'Failed to call designated initializer on NSManagedObject class' NSManagedObject and NSCoding

I'm attempting to:
Load an array of custom objects from JSON file.
Save it in CoreData.
Fetch it using NSFetchRequest.
Note: Each element in the array of custom objects named 'Cube' also contains a nested array of custom objects named 'Algorithm', which also conform to NSCoding (shown in code snippet below).
Steps 1 + 2 seem to work fine. The error occurs when fetching the top level Entities named 'Cube', specifically right after the self.init() is called inside the nested class 'Algorithm'.
Algorithm class:
#objc(Algorithm)
public class Algorithm: NSManagedObject, Decodable, Encodable, NSSecureCoding {
enum CodingKeys: String, CodingKey {
case imageNumber, alg, alternativeAlgs, type, tags, isFavorite, optimalMoves, name }
public static var supportsSecureCoding = true
public func encode(with coder: NSCoder) {
coder.encode(imageNumber, forKey: CodingKeys.imageNumber.rawValue)
coder.encode(optimalMoves, forKey: CodingKeys.optimalMoves.rawValue)
coder.encode(alg, forKey: CodingKeys.alg.rawValue)
coder.encode(name, forKey: CodingKeys.name.rawValue)
coder.encode(type, forKey: CodingKeys.type.rawValue)
coder.encode(isFavorite, forKey: CodingKeys.isFavorite.rawValue)
coder.encode(alternativeAlgs, forKey: CodingKeys.alternativeAlgs.rawValue)
coder.encode(tags, forKey: CodingKeys.tags.rawValue) }
required convenience public init?(coder: NSCoder) {
self.init()
imageNumber = coder.decodeObject(forKey: CodingKeys.imageNumber.rawValue) as? String ?? ""
optimalMoves = coder.decodeObject(forKey: CodingKeys.optimalMoves.rawValue) as? String ?? ""
alg = coder.decodeObject(forKey: CodingKeys.alg.rawValue) as? String ?? ""
name = coder.decodeObject(forKey: CodingKeys.name.rawValue) as? String ?? ""
type = coder.decodeObject(forKey: CodingKeys.type.rawValue) as? String ?? ""
isFavorite = coder.decodeBool(forKey: CodingKeys.isFavorite.rawValue)
alternativeAlgs = coder.decodeObject(forKey: CodingKeys.alternativeAlgs.rawValue) as? [String] ?? []
tags = coder.decodeObject(forKey: CodingKeys.tags.rawValue) as? [String] ?? [] }
required convenience public init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
throw DecoderConfigurationError.missingManagedObjectContext
}
self.init(context: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.imageNumber = try container.decode(String.self, forKey: .imageNumber)
self.optimalMoves = try container.decode(String.self, forKey: .optimalMoves)
self.alg = try container.decode(String.self, forKey: .alg)
self.name = try container.decode(String.self, forKey: .name)
self.type = try container.decode(String.self, forKey: .type)
self.isFavorite = try container.decode(Bool.self, forKey: .isFavorite)
self.alternativeAlgs = try container.decode([String].self, forKey: .alternativeAlgs) as [String]
self.tags = try container.decode([String].self, forKey: .tags) as [String] }
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(imageNumber, forKey: .imageNumber)
try container.encode(optimalMoves, forKey: .optimalMoves)
try container.encode(alg, forKey: .alg)
try container.encode(name, forKey: .name)
try container.encode(type, forKey: .type)
try container.encode(isFavorite, forKey: .isFavorite)
try container.encode(alternativeAlgs, forKey: .alternativeAlgs)
try container.encode(tags, forKey: .tags) }
}
extension Algorithm {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Algorithm> {
return NSFetchRequest<Algorithm>(entityName: "Algorithm")
}
#NSManaged public var alg: String
#NSManaged public var alternativeAlgs: [String]
#NSManaged public var imageNumber: String
#NSManaged public var isFavorite: Bool
#NSManaged public var name: String
#NSManaged public var optimalMoves: String
#NSManaged public var tags: [String]
#NSManaged public var type: String
#NSManaged public var cube: Cube?
}
extension Algorithm : Identifiable {
}
After reading people's comments I am aware I should replace the self.init() with NSManagedObject's designated init, but I haven't found the right way to do so. The app crashes after replacing the self.init() with the following code:
let context = CoreDataManager.shared.container.viewContext
self.init(context: context)
// self.init()
FWI - in the CoreData.xcdatamodeled file inspector the algorithms array is defined as Transformable with a custom transformer - AlgorithmDataTransformer:
#objc(AlgorithmDataTransformer)
public final class AlgorithmDataTransformer: ValueTransformer {
override public func transformedValue(_ value: Any?) -> Any? {
guard let array = value as? [Algorithm] else { return nil }
do {
return try NSKeyedArchiver.archivedData(withRootObject: array, requiringSecureCoding: true)
} catch {
assertionFailure("Failed to transform `Algorithm` to `Data`")
return nil
}
}
override public func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? NSData else { return nil }
do {
return try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: Algorithm.self, from: data as Data)
} catch {
assertionFailure("Failed to transform `Data` to `Algorithm`")
return nil
}
}
}
extension AlgorithmDataTransformer {
/// The name of the transformer. This is the name used to register the transformer using `ValueTransformer.setValueTrandformer(_"forName:)`.
static let name = NSValueTransformerName(rawValue: String(describing: AlgorithmDataTransformer.self))
/// Registers the value transformer with `ValueTransformer`.
public static func register() {
let transformer = AlgorithmDataTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}

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.

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")

How to encode/decode a dictionary with Codable values for storage in UserDefaults?

I am trying to store a dictionary of company names (string) mapped to Company objects (from a struct Company) in iOS UserDefaults. I have created the Company struct and made it conform to Codable. I have one example a friend helped me with in my project where we created a class Account and stored it in UserDefaults by making a Defaults struct (will include example code). I have read in the swift docs that dictionaries conform to Codable and in order to stay Codable, must contain Codable objects. That is why I made struct Company conform to Codable.
I have created a struct for Company that conforms to Codable. I have tried using model code to create a new struct CompanyDefaults to handle the getting and setting of the Company dictionary from/to UserDefaults. I feel I have some beginner misconceptions about what needs to happen and about how it should be implemented (with good design in mind).
The dictionary I wish to store looks like [String:Company]
where company name will be String and a Company object for Company
I used conform to Codable as I did some research and it seemed like a newer method for completing similar tasks.
Company struct
struct Company: Codable {
var name:String?
var initials:String? = nil
var logoURL:URL? = nil
var brandColor:String? = nil // Change to UIColor
enum CodingKeys:String, CodingKey {
case name = "name"
case initials = "initials"
case logoURL = "logoURL"
case brandColor = "brandColor"
}
init(name:String?, initials:String?, logoURL:URL?, brandColor:String?) {
self.name = name
self.initials = initials
self.logoURL = logoURL
self.brandColor = brandColor
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
initials = try values.decode(String.self, forKey: .initials)
logoURL = try values.decode(URL.self, forKey: .logoURL)
brandColor = try values.decode(String.self, forKey: .brandColor)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(initials, forKey: .initials)
try container.encode(logoURL, forKey: .logoURL)
try container.encode(brandColor, forKey: .brandColor)
}
}
Defaults struct to control storage
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
let companies = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [String : Company] ?? [:]
return companies!
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: companies, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}
I should be able to reference the stored dictionary throughout my code like CompanyDefaults.companies.count
For reference, a friend helped me do a similar task for an array of Account classes stored in user defaults. The code that works perfectly for that is below. The reason I tried a different way is that I had a different data structure (dictionary) and made the decision to use structs.
class Account: NSObject, NSCoding {
let service: String
var username: String
var password: String
func encode(with aCoder: NSCoder) {
aCoder.encode(service)
aCoder.encode(username)
aCoder.encode(password)
}
required init?(coder aDecoder: NSCoder) {
guard let service = aDecoder.decodeObject() as? String,
var username = aDecoder.decodeObject() as? String,
var password = aDecoder.decodeObject() as? String else {
return nil
}
self.service = service
self.username = username
self.password = password
}
init(service: String, username: String, password: String) {
self.service = service
self.username = username
self.password = password
}
}
struct Defaults {
static private let accountsKey = "accountsKey"
static var accounts: [Account] = {
guard let data = UserDefaults.standard.data(forKey: accountsKey) else { return [] }
let accounts = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Account] ?? []
return accounts
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: accounts, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: accountsKey)
}
}
}
You are mixing up NSCoding and Codable. The former requires a subclass of NSObject, the latter can encode the structs and classes directly with JSONEncoder or ProperListEncoder without any Keyedarchiver which also belongs to NSCoding.
Your struct can be reduced to
struct Company: Codable {
var name : String
var initials : String
var logoURL : URL?
var brandColor : String?
}
That's all, the CodingKeys and the other methods are synthesized. I would at least declare name and initials as non-optional.
To read and save the data is pretty straightforward. The corresponding CompanyDefaults struct is
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
return try? JSONDecoder.decode([String:Company].self, from: data) ?? [:]
}() {
didSet {
guard let data = try? JSONEncoder().encode(companies) else { return }
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}