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