Save Struct in Class to Nsuserdefaults Using Swift

NSUserDefaults: storing struct/class with only property list compliant types, in a readable format

The following extension to UserDefaults solves the problem, and I didn't generalize it for lack of time, but it may be possible:

extension UserDefaults {
func set(_ preset: Preset, forKey key: String) {
set(["name": preset.name, "value": preset.value], forKey: key)
}
}

This can work on arrays as well:

extension UserDefaults {
func set(_ presets: [Preset], forKey key: String) {
let result = presets.map { ["name":$0.name, "value":$0.value] }
set(result, forKey: key)
}
}

While UserDefaults.standard.set(:forKey:) is what the question was about, my goal was actually to get it working with Cocoa bindings for use with NSArrayController. I decided to subclass NSArrayController as follows (see comment by Hamish to my other question, which was the last missing piece of the puzzle to make this generic):

extension Encodable {
fileprivate func encode(to container: inout SingleValueEncodingContainer) throws {
try container.encode(self)
}
}

struct AnyEncodable: Encodable {
var value: Encodable
init(_ value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try value.encode(to: &container)
}
}

class NSEncodableArrayController: NSArrayController {
override func addObject(_ object: Any) {
let data = try! PropertyListEncoder().encode(AnyEncodable(object as! Encodable))
let any = try! PropertyListSerialization.propertyList(from: data, options: [], format: nil)

super.addObject(any)
}
}

Save Struct to UserDefaults

In Swift 4 this is pretty much trivial. Make your struct codable simply by marking it as adopting the Codable protocol:

struct Song:Codable {
var title: String
var artist: String
}

Now let's start with some data:

var songs: [Song] = [
Song(title: "Title 1", artist: "Artist 1"),
Song(title: "Title 2", artist: "Artist 2"),
Song(title: "Title 3", artist: "Artist 3"),
]

Here's how to get that into UserDefaults:

UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")

And here's how to get it back out again later:

if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}

How Do I Save an Array of Objects to NSUserDefaults?

To save a structure in UserDefaults you need to first encode it to be able to save it as Data. So you need to make your custom structure conform to Codable:

struct Event: Codable {
let id: UUID
let name: String
let start, end: Date
let fromTime, toTime: String
let color: Color
init(id: UUID = .init(),
name: String,
start: Date,
end: Date,
fromTime: String,
toTime: String,
color: Color) {
self.id = id
self.name = name
self.start = start
self.end = end
self.fromTime = fromTime
self.toTime = toTime
self.color = color
}
}

Note that you can not conform UIColor to Codable but you can create a custom Color structure:

struct Color: Codable {
let (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat)
}


extension Color {
init?(_ uiColor: UIColor) {
var (r, g, b, a): (CGFloat,CGFloat,CGFloat,CGFloat) = (0, 0, 0, 0)
guard uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) else { return nil }
self.init(r: r, g: g, b: b, a: a)
}
var color: UIColor { .init(red: r, green: g, blue: b, alpha: a) }
}


extension UIColor {
convenience init(_ color: Color) {
self.init(red: color.r, green: color.g, blue: color.b, alpha: color.a)
}
var color: Color? { Color(self) }
}

Regarding your class you can also make it conform to Codable or inherit from NSObject and conform to NSCoding:

class Events: NSObject, NSCoding {
private override init() { }
static var shared = Events()
var events: [Event] = []
required init(coder decoder: NSCoder) {
events = try! JSONDecoder().decode([Event].self, from: decoder.decodeData()!)
}
func encode(with coder: NSCoder) {
try! coder.encode(JSONEncoder().encode(events))
}
}

Playground testing:

Events.shared.events = [.init(name: "a",
start: Date(),
end: Date(),
fromTime: "fromTime",
toTime: "toTime",
color: .init(r: 0, g: 0, b: 1, a: 1)),
.init(name: "b",
start: Date(),
end: Date(),
fromTime: "fromTimeB",
toTime: "toTimeB",
color: .init(r: 0, g: 1, b: 0, a: 1))]

print(Events.shared.events)
let data = try! NSKeyedArchiver.archivedData(withRootObject: Events.shared, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "events")
Events.shared.events = []
print(Events.shared.events)
let loadedData = UserDefaults.standard.data(forKey: "events")!
Events.shared = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(loadedData) as! Events
print(Events.shared.events)

This will print

[Event(id: C7D9475B-773E-4272-84CC-56CAEAA73D0C, name: "a", start: 2021-01-26 05:17:30 +0000, end: 2021-01-26 05:17:30 +0000, fromTime: "fromTime", toTime: "toTime", color: Color(r: 0.0, g: 0.0, b: 1.0, a: 1.0)), Event(id: 0BEA4225-2F63-4EEB-AF10-F3EF4C84D050, name: "b", start: 2021-01-26 05:17:30 +0000, end: 2021-01-26 05:17:30 +0000, fromTime: "fromTimeB", toTime: "toTimeB", color: Color(r: 0.0, g: 1.0, b: 0.0, a: 1.0))]

[]

[Event(id: C7D9475B-773E-4272-84CC-56CAEAA73D0C, name: "a", start: 2021-01-26 05:17:30 +0000, end: 2021-01-26 05:17:30 +0000, fromTime: "fromTime", toTime: "toTime", color: Color(r: 0.0, g: 0.0, b: 1.0, a: 1.0)), Event(id: 0BEA4225-2F63-4EEB-AF10-F3EF4C84D050, name: "b", start: 2021-01-26 05:17:30 +0000, end: 2021-01-26 05:17:30 +0000, fromTime: "fromTimeB", toTime: "toTimeB", color: Color(r: 0.0, g: 1.0, b: 0.0, a: 1.0))]

Array of structs: UserDefaults, how to use?

Since both types contain only property list compliant types a suitable solution is to add code to convert each type to a property list compliant object and vice versa.

struct MySection {
var name: String
var values = [MyRow]()

init(name : String, values : [MyRow] = []) {
self.name = name
self.values = values
}

init(propertyList: [String: Any]) {
self.name = propertyList["name"] as! String
self.values = (propertyList["values"] as! [[String:String]]).map{ MyRow(propertyList: $0) }
}

var propertyListRepresentation : [String: Any] {
return ["name" : name, "values" : values.map { $0.propertyListRepresentation }]
}
}

struct MyRow {
var value: String
var quantity: String
var quantityType: String
var done: String

init(value : String, quantity: String, quantityType: String, done: String) {
self.value = value
self.quantity = quantity
self.quantityType = quantityType
self.done = done
}

init(propertyList: [String:String]) {
self.value = propertyList["value"]!
self.quantity = propertyList["quantity"]!
self.quantityType = propertyList["quantityType"]!
self.done = propertyList["done"]!
}

var propertyListRepresentation : [String: Any] {
return ["value" : value, "quantity" : quantity, "quantityType" : quantityType, "done" : done ]
}
}

After creating a few objects

let row1 = MyRow(value: "Foo", quantity: "10", quantityType: "Foo", done: "Yes")
let row2 = MyRow(value: "Bar", quantity: "10", quantityType: "Bar", done: "No")

let section = MySection(name: "Baz", values: [row1, row2])

call propertyListRepresentation to get a dictionary ([String:Any]) which can be saved to User Defaults.

let propertyList = section.propertyListRepresentation

Recreation of the section is quite easy, too

let newSection = MySection(propertyList: propertyList)

Edit

Use the propertyList initializer only if you get data from UserDefaults in all other cases use the other initializer.

For example replace

@IBAction func addButtonPressed(_ sender: Any) {
newProducts.append(MyRow(propertyList: ["":""]))
newProducts[newProducts.count - 1].value = nameTextField.text!
newProducts[newProducts.count - 1].quantity = quantityTextField.text!
newProducts[newProducts.count - 1].quantityType = type
newProducts[newProducts.count - 1].done = "No"
delegate?.returnInfos(newItem: newProducts, sectionPick: typePick)
navigationController?.popViewController(animated: true)
}

with

@IBAction func addButtonPressed(_ sender: Any) {

let row = MyRow(value: nameTextField.text!,
quantity: quantityTextField.text!,
quantityType: type,
done: "No")
newProducts.append(row)
delegate?.returnInfos(newItem: newProducts, sectionPick: typePick)
navigationController?.popViewController(animated: true)
}

and replace

func returnInfos(newItem: [MyRow], sectionPick: String) {
arrayRow.append(MyRow(propertyList: ["":""]))
arrayRow[arrayRow.count - 1] = newItem[0]
manageSection(item: sectionPick)
listTableView.reloadData()
}

with

func returnInfos(newItem: [MyRow], sectionPick: String) {
arrayRow.append(newItem[0])
manageSection(item: sectionPick)
listTableView.reloadData()
}

Basically first create the object, then append it to the array. The other way round is very cumbersome.



Related Topics



Leave a reply



Submit