Storing Array of Custom Objects in Userdefaults

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"}]

How to store array list of custom objects to NSUserDefaults?

You need to implement protocol NSCoding for your custom class. Please see this Swift example.

Swift 4: Save and retrieve an array of custom objects (with nested custom objects) to UserDefaults

Go ahead and implement NSCoding for the other two custom objects. Also, change your decodeObject to decodeInteger on all Integers of your custom objects, and remove the "as! Int" from them. Then, do this:

let userDefaults = UserDefaults.standard
let encodedData = NSKeyedArchiver.archivedData(withRootObject: self.saleArray)
userDefaults.set(encodedData, forKey: "sales")

To retrieve the data, do this:

let newArray = NSKeyedUnarchiver.unarchiveObject(with: data as! Data) as! [SaleObject]

After you have it working, go back and research Codable. Enjoy!

SwiftUI store and update Custom object in UserDefault array

In your project Dog is a class. Your array list, in your DogArray class, only stores references. When the properties of a Dog change, its reference remains unchanged, the array doesn't change and the didSet is not called.

I simplified your project to make the problem more understandable:

struct DogList: View {
@StateObject var dogs = DogArray(list: ["John", "Bob"].map(Dog.init))
var body: some View {
NavigationView {
List(dogs.list.indices) {index in
NavigationLink(destination: DogDetail(dog: dogs.list[index])) {
Text(dogs.list[index].name)
Image(systemName: "heart.fill")
.foregroundColor(dogs.list[index].favorite ? .red : .gray)
}
}
}
}
}

struct DogDetail: View {
@ObservedObject var dog: Dog
var body: some View {
VStack {
Text(dog.name)

Button {
dog.favorite.toggle()
} label: {
Image(systemName: "heart.fill")
.foregroundColor(dog.favorite ? .red : .gray)
}
}.font(.largeTitle)
}
}

class Dog: ObservableObject {
let id = UUID()
var name: String
@Published var favorite = false
init(_ name: String) {
self.name = name
}
}

class DogArray: ObservableObject {
@Published var list: [Dog] {
didSet {
print("Array changes")
}
}
init(list: [Dog]) {
self.list = list
}
}

Here, when the user taps the heart in the DogDetail View, the Dog changed, the DogDetail's body is redrawn (because Dog is observable). But the DogList is not invalidated. The parent view does not know that the Dog has changed.

We have only three things to do to resolve problem.

  1. Make Dog a struct
  2. @ObservedObject var dog: Dog -> @Binding var dog: Dog
  3. DogDetail(dog: dogs.list[index]) -> DogDetail(dog: $dogs.list[index])

Now it works.

Your problem is a little more complicated than this example because you use ObservableObject for three things: your Model (here Dog : to be avoided), your Store (here DogArray), and a ViewModel (which I can't see well the usefulness).

You should choose I think between using a global Store (your DogArray) that you inject with EnvironmentObject at the top of your view hierarchy. Views that need it get it. All share the same object.

Or really switch to an architecture with ViewModels, and in this case it's up to each of them to manage access to the Model.

It could go something like this:

import SwiftUI
struct DogList: View {
class ViewModel: ObservableObject {
@Published var list: [DogDetail.ViewModel]
init(dogs: [Dog]) {
list = []
list = dogs.map { DogDetail.ViewModel(dog: $0, parent: self) }
}
func save() {
print("list saved")
}
}
@StateObject var vm = DogList.ViewModel(dogs:["John", "Bob"].map(Dog.init))
var body: some View {
NavigationView {
List(vm.list, id: \.dog.id) {detail in
NavigationLink(destination: DogDetail(vm: detail)) {
DogRow(vm: detail)
}
}
}
}
}

struct DogRow: View {
@ObservedObject var vm: DogDetail.ViewModel
var body: some View {
Text(vm.dog.name)
Image(systemName: "heart.fill")
.foregroundColor(vm.dog.favorite ? .red : .gray)
}
}

struct DogDetail: View {
class ViewModel: ObservableObject {
let parent: DogList.ViewModel
@Published var dog: Dog {
didSet {
print("a dog changed")
parent.objectWillChange.send()
parent.save()
}
}
init(dog: Dog, parent: DogList.ViewModel) {
self.dog = dog
self.parent = parent
}
}

@ObservedObject var vm: ViewModel
var dog: Dog { vm.dog }
var body: some View {
VStack {
Text(dog.name)
Button {
vm.dog.favorite.toggle()
} label: {
Image(systemName: "heart.fill")
.foregroundColor(dog.favorite ? .red : .gray)
}
}.font(.largeTitle)
}
}

Edit (Combine)

You may not like when the "parent" gives a reference of itself to its "children" : DogDetail.ViewModel(dog: $0, parent: self)

You can also use Combine and make DogList.ViewModel subscribe to changes in DogDetail.ViewModel.dog :

import Combine
struct DogList: View {
class ViewModel: ObservableObject {
@Published var list: [DogDetail.ViewModel] {
didSet {
subscribeToChanges()
}
}
func subscribeToChanges() {
self.cancellable = list.publisher
.flatMap { $0.dogDidChange }
.sink { [weak self] _ in
self?.objectWillChange.send()
self?.save()
}
}
var cancellable: AnyCancellable?
init(dogs: [Dog]) {
list = dogs.map { DogDetail.ViewModel(dog: $0) }
subscribeToChanges()
}
func save() {
print("list saved :")
for dogVM in list {
print(dogVM.dog.favorite)
}
}
}
// ....//
}

You need a dogDidChange publisher in DogDetail.ViewModel :

struct DogDetail: View {
class ViewModel: ObservableObject {
var dogDidChange = PassthroughSubject<Void, Never>()
@Published var dog: Dog {
didSet {
print("a dog changed")
dogDidChange.send()
}
}
init(dog: Dog) {
self.dog = dog
}
}
//...//
}

Saving Array of Custom Object

You are trying to save an array of custom objects to UserDefaults. Your custom object isn't a property list object You should use Codable to save non-property list object in UserDefaults like this.

Swift 4

Custom Class

class Sheet: Codable {
var title = ""
var content = ""
}

ViewController.swift

class ViewController: UIViewController {

var notes = [Sheet]()

override func viewDidLoad() {
super.viewDidLoad()

getSheets()
addSheets()
getSheets()
}
func getSheets()
{
if let storedObject: Data = UserDefaults.standard.data(forKey: "notes")
{
do
{
notes = try PropertyListDecoder().decode([Sheet].self, from: storedObject)
for note in notes
{
print(note.title)
print(note.content)
}
}
catch
{
print(error.localizedDescription)
}
}
}
func addSheets()
{
let sheet1 = Sheet()
sheet1.title = "title1"
sheet1.content = "content1"

let sheet2 = Sheet()
sheet2.title = "title1"
sheet2.content = "content1"

notes = [sheet1,sheet2]
do
{
UserDefaults.standard.set(try PropertyListEncoder().encode(notes), forKey: "notes")
UserDefaults.standard.synchronize()
}
catch
{
print(error.localizedDescription)
}
}
}

Unable to store an array of custom objects in UserDefaults

The reason for this error is located in your getBadges() function:

func getBadges() -> [Badge] {
return self.defaults.array(forKey: BadgeFactory.b) as! [Badge]
}

With as! you are implicitly unwrapping the array you expect. But as long as you didn't write data to this userDefaults key, array(forKey:) will always return nil!

For this reason, you need to use safe unwrapping here, for example like so:

return self.defaults.array(forKey: BadgeFactory.b) as? [Badge] ?? [].


But that's not the only problem. Like you already stumbled about, you still need to implement the solution of the thread you posted. Custom NSObjects cannot be stored in Defaults without encoding.

You need to implement the NSCoding protocol in your Badge class (init(coder:) is missing) and use an Unarchiver for reading, along with an Archiver for writing your data to defaults.

So your code should look something like this:

class Badge: NSObject, NSCoding {
var name: String
var info: String
var score: Float?

init(name: String, info: String, score: Float?) {
self.name = name
self.info = info
self.score = score
}

required init?(coder: NSCoder) {
self.name = coder.decodeObject(forKey: "name") as! String
self.info = coder.decodeObject(forKey: "info") as! String
self.score = coder.decodeObject(forKey: "score") as? Float
}

static func ==(lhs: Badge, rhs: Badge) -> Bool {
return lhs.name == rhs.name
}

func encodeWithCoder(coder: NSCoder) {
coder.encode(self.name, forKey: "name")
coder.encode(self.info, forKey: "info")
coder.encode(self.score, forKey: "score")
}
}
class BadgeFactory {
...

func addBadges(score: Float) -> [Badge] {
...
let data = NSKeyedArchiver.archivedData(withRootObject: self.userBadges)
defaults.set(data, forKey: BadgeFactory.b)
...
}

func getBadges() -> [Badge] {
guard let data = defaults.data(forKey: BadgeFactory.b) else { return [] }
return NSKeyedUnarchiver.unarchiveObject(ofClass: Badge, from: data) ?? []
}

...
}

How to use NSUserDefaults to store an array of custom classes in Swift?

Your Person class should look like this:

Swift 3:

class Person : NSObject, NSCoding{

// Person dictionary variable
var name: String?
var age: String?
var html_url: String?

init(json: NSDictionary) { // Dictionary object
self.name = json["name"] as? String
self.age = json["age"] as? String
self.html_url = json["html_url"] as? String // Location of the JSON file
}

required init?(coder aDecoder: NSCoder) {

self.name = aDecoder.decodeObjectForKey("name") as? String;
self.age = aDecoder.decodeObjectForKey("age") as? String;
self.html_url = aDecoder.decodeObjectForKey("html") as? String;
}

func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.name, forKey: "name");
aCoder.encodeObject(self.age, forKey: "age");
aCoder.encodeObject(self.html_url, forKey: "html");
}
}

And here you have an example of saving and retrieving the array from NSUserDefaults:

let p = Person()
p.name = "person1"
p.age = "12"
p.html_url = "www.google.ro"

let p2 = Person()
p2.name = "person2"
p2.age = "11"
p2.html_url = "www.google.ro"

let array = [p, p2]

let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setValue(NSKeyedArchiver.archivedDataWithRootObject(array), forKey: "persons")
userDefaults.synchronize()

let array : [Person]
array = NSKeyedUnarchiver.unarchiveObjectWithData(userDefaults.objectForKey("persons") as! NSData) as! [Person]
print("\(array[0].name)\(array[1].name)")

Swift 4:

class Person : NSObject, NSCoding{

// Person dictionary variable
var name: String?
var age: String?
var html_url: String?

init(json: NSDictionary) { // Dictionary object
self.name = json["name"] as? String
self.age = json["age"] as? String
self.html_url = json["html_url"] as? String // Location of the JSON file
}

required init?(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObject(forKey: "name") as? String;
self.age = aDecoder.decodeObject(forKey: "age") as? String;
self.html_url = aDecoder.decodeObject(forKey: "html") as? String;
}

func encode(with aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "name");
aCoder.encode(self.age, forKey: "age");
aCoder.encode(self.html_url, forKey: "html");
}
}

Saving Custom Array To User Defaults

Found answer at Apple Developer Website and then converted to swift.

First I archived it with NSKeyedArchiver then saved it to UserDefaults:

questions.remove(at: questionNumber)
//Archiving Custom Object Array Into NSKeyedArchiver And Then Saving NSData To UserDefaults
let theData = NSKeyedArchiver.archivedData(withRootObject: questions)
UserDefaults.standard.set(theData, forKey: "questionData")

Then I retrieved it in viewDidLoad by unarchiving it with NSKeyedUnarchiver then got it from UserDefaults:

override func viewDidLoad() {
super.viewDidLoad()
let theData: Data? = UserDefaults.standard.data(forKey: "questionData")
if theData != nil {
questions = (NSKeyedUnarchiver.unarchiveObject(with: theData!) as? [question])!
}
}


Related Topics



Leave a reply



Submit