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
Why Swift Use a Struct as Dictionary Key Instead of a String Here
Error "Call Can Throw, But Is Not Marked with 'Try' and the Error Is Not Handled"
Show More Button Next to End of Text Swift
Creating an Irregular Uibutton in Swift Where Transparent Parts Are Not Tappable
Passing Data Between Views in One Viewcontroller in Swift
Uitapgesturerecognizer Called Immediately
No Such Module "Sinch" Xcode 9.1 Swift 4
Uitabbar Transition Issue Below iOS 11 Swift
Sorting Struct Array in Swift 4
Impelementation of Rtcdatachannel of Webrtc in iOS
Retrieving Uiimage from Uiimageview in Swift
How to Change a View from Portrait Mode to Landscape Mode and Lock It