Struct Array to Userdefaults

Saving an array of structs to userDefaults

Like this:

struct Item : Codable {
var itemType: clothingType
var itemName: String
}

enum clothingType : String, Codable {
case pants
case shirt
case jacket
case shoes
case hat
case extra
}

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let defaults = UserDefaults.standard
if let data = defaults.data(forKey: "SavedItemArray") {
let array = try! PropertyListDecoder().decode([Item].self, from: data)
}
}
}

Of course I suppose now you're wondering how we got the data into user defaults in the first place. Like this:

    let array : [Item] = // whatever
if let data = try? PropertyListEncoder().encode(array) {
UserDefaults.standard.set(data, forKey: "SavedItemArray")
}

STRUCT Array To UserDefaults

To be able to use NSCoding the object must be a class. But as all values are property list compliant you could add a variable dictionaryRepresentation and a corresponding initializer.

First of all never use NSMutableArray in Swift and never declare variables as implicit unwrapped optional which are initialized with a non-optional initializer.

var theArray = [CFCPstruct]()

struct CFCPstruct {
let calories : Int
let fats : Int
let carbs : Int
let protein: Int

init(calories: Int, fats: Int, carbs: Int, protein: Int) {
self.calories = calories
self.fats = fats
self.carbs = carbs
self.protein = protein
}

init(dictionary : [String:Int]) {
self.calories = dictionary["calories"]!
self.fats = dictionary["fats"]!
self.carbs = dictionary["carbs"]!
self.protein = dictionary["protein"]!
}

var dictionaryRepresentation : [String:Int] {
return ["calories" : calories, "fats" : fats, "carbs" : carbs, "protein" : protein]
}
}

Now you can read the array from and write to user defaults.

func saveDefaults() 
{
let cfcpArray = theArray.map{ $0.dictionaryRepresentation }
UserDefaults.standard.set(cfcpArray, forKey: "cfcpArray")
}

func loadDefaults()
{
theArray = (UserDefaults.standard.object(forKey: "cfcpArray") as! [[String:Int]]).map{ CFCPstruct(dictionary:$0) }
}

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

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.

Decode an Array of an custom struct in Swift out of the UserDefaults

You just need to pass your custom structure array type [MyStruct].self to the JSONDecoder:



struct MyStruct: Codable {
let int: Int
let string: String
}


let myStructures: [MyStruct] = [.init(int: 2, string: "Example"),
.init(int: 5, string: "Other Example")]


do {
let encodedData = try JSONEncoder().encode(myStructures)
UserDefaults.standard.set(encodedData, forKey: "array")
if let decodedData = UserDefaults.standard.data(forKey: "array") {
let decodedArray = try JSONDecoder().decode([MyStruct].self, from: decodedData)
print(decodedArray)
}
} catch {
print(error)
}

This will print:

[MyStruct(int: 2, string: "Example"), MyStruct(int: 5, string: "Other Example")]

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

Storing array of custom objects in UserDefaults

If you really want to persist your data using UserDefaults the easiest way would be to use a class and conform it to NSCoding. Regarding your global var domainSchemas I would recommend using a singleton or extend UserDefaults and create a computed property for it there:



class DomainSchema: NSObject, NSCoding {
var domain: String
var schema: String
init(domain: String, schema: String) {
self.domain = domain
self.schema = schema
}
required init(coder decoder: NSCoder) {
self.domain = decoder.decodeObject(forKey: "domain") as? String ?? ""
self.schema = decoder.decodeObject(forKey: "schema") as? String ?? ""
}
func encode(with coder: NSCoder) {
coder.encode(domain, forKey: "domain")
coder.encode(schema, forKey: "schema")
}
}


extension UserDefaults {
var domainSchemas: [DomainSchema] {
get {
guard let data = UserDefaults.standard.data(forKey: "domainSchemas") else { return [] }
return (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? [DomainSchema] ?? []
}
set {
UserDefaults.standard.set(try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false), forKey: "domainSchemas")
}
}
}

Usage:

UserDefaults.standard.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]

UserDefaults.standard.domainSchemas // [{NSObject, domain "a", schema "b"}, {NSObject, domain "c", schema "d"}]



If you prefer the Codable approach persisting the Data using UserDefaults as well:



struct DomainSchema: Codable {
var domain: String
var schema: String
init(domain: String, schema: String) {
self.domain = domain
self.schema = schema
}
}


extension UserDefaults {
var domainSchemas: [DomainSchema] {
get {
guard let data = UserDefaults.standard.data(forKey: "domainSchemas") else { return [] }
return (try? PropertyListDecoder().decode([DomainSchema].self, from: data)) ?? []
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "domainSchemas")
}
}
}

Usage:

UserDefaults.standard.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]

UserDefaults.standard.domainSchemas // [{domain "a", schema "b"}, {domain "c", schema "d"}]

I think the best option would be to do not use UserDefaults, create a singleton "shared instance", declare a domainSchemas property there and save your json Data inside a subdirectory of you application support directory:

extension URL {
static var domainSchemas: URL {
let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let bundleID = Bundle.main.bundleIdentifier ?? "company name"
let subDirectory = applicationSupport.appendingPathComponent(bundleID, isDirectory: true)
try? FileManager.default.createDirectory(at: subDirectory, withIntermediateDirectories: true, attributes: nil)
return subDirectory.appendingPathComponent("domainSchemas.json")
}
}


class Shared {
static let instance = Shared()
private init() { }
var domainSchemas: [DomainSchema] {
get {
guard let data = try? Data(contentsOf: .domainSchemas) else { return [] }
return (try? JSONDecoder().decode([DomainSchema].self, from: data)) ?? []
}
set {
try? JSONEncoder().encode(newValue).write(to: .domainSchemas)
}
}
}

Usage:

Shared.instance.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]

Shared.instance.domainSchemas // [{domain "a", schema "b"}, {domain "c", schema "d"}]


Related Topics



Leave a reply



Submit