Saving Custom Swift Class With Nscoding to Userdefaults

Saving custom Swift class with NSCoding to UserDefaults

As @dan-beaulieu suggested I answer my own question:

Here is the working code now:

Note: Demangling of the class name was not necessary for the code to work in Playgrounds.

import Foundation

class Blog : NSObject, NSCoding {

var blogName: String?

override init() {}

required init(coder aDecoder: NSCoder) {
if let blogName = aDecoder.decodeObjectForKey("blogName") as? String {
self.blogName = blogName
}
}

func encodeWithCoder(aCoder: NSCoder) {
if let blogName = self.blogName {
aCoder.encodeObject(blogName, forKey: "blogName")
}
}

}

let ud = NSUserDefaults.standardUserDefaults()

var blog = Blog()
blog.blogName = "My Blog"

ud.setObject(NSKeyedArchiver.archivedDataWithRootObject(blog), forKey: "blog")

if let data = ud.objectForKey("blog") as? NSData {
let unarc = NSKeyedUnarchiver(forReadingWithData: data)
let newBlog = unarc.decodeObjectForKey("root") as Blog
}

Swift saving and retrieving custom object from UserDefaults

Swift 4 or later

You can once again save/test your values in a Playground


UserDefaults need to be tested in a real project. Note: No need to force synchronize. If you want to test the coding/decoding in a playground you can save the data to a plist file in the document directory using the keyed archiver. You need also to fix some issues in your class:



class Person: NSObject, NSCoding {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
required init(coder decoder: NSCoder) {
self.name = decoder.decodeObject(forKey: "name") as? String ?? ""
self.age = decoder.decodeInteger(forKey: "age")
}
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(age, forKey: "age")
}
}

Testing:

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

do {
// setting a value for a key
let newPerson = Person(name: "Joe", age: 10)
var people = [Person]()
people.append(newPerson)
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: people, requiringSecureCoding: false)
UserDefaults.standard.set(encodedData, forKey: "people")
// retrieving a value for a key
if let data = UserDefaults.standard.data(forKey: "people"),
let myPeopleList = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Person] {
myPeopleList.forEach({print($0.name, $0.age)}) // Joe 10
}
} catch {
print(error)
}

}
}

Save custom objects into NSUserDefaults

Actually, you will need to archive the custom object into NSData then save it to user defaults and retrieve it from user defaults and unarchive it again.
You can archive it like this

let teams = [Team(id: 1, name: "team1", shortname: "t1"), Team(id: 2, name: "team2", shortname: "t2")]

var userDefaults = UserDefaults.standard
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: teams)
userDefaults.set(encodedData, forKey: "teams")

and unarchive it like this

let decoded  = userDefaults.data(forKey: "teams")
let decodedTeams = NSKeyedUnarchiver.unarchiveObject(with: decoded) as! [Team]

But if you just did that you will get

.Team encodeWithCoder:]: unrecognized selector sent to instance

You will have to make Team conform to NSCoding just like this

class Team: NSObject, NSCoding {
var id: Int
var name: String
var shortname: String


init(id: Int, name: String, shortname: String) {
self.id = id
self.name = name
self.shortname = shortname

}

required convenience init(coder aDecoder: NSCoder) {
let id = aDecoder.decodeInteger(forKey: "id")
let name = aDecoder.decodeObject(forKey: "name") as! String
let shortname = aDecoder.decodeObject(forKey: "shortname") as! String
self.init(id: id, name: name, shortname: shortname)
}

func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(name, forKey: "name")
aCoder.encode(shortname, forKey: "shortname")
}
}

How can I save an object of a custom class in Userdefaults in swift 5/ Xcode 10.2

Vadian's answer is correct, you cannot use NSKeyedArchiver with structs. Having all your objects conform to Codable is the best way to reproduce the behavior you are looking for. I do what Vadian does, but I you can also use protocol extensions to make this safer.

import UIKit

struct Patient: Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}

struct Diagnose: Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}

struct Treatment: Codable {
var name: String
var wasMade : Bool
}

let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]

Introduce a protocol to manage the encoding and saving of objects.

This does not have to inherit from Codable but it does for this example for simplicity.

/// Objects conforming to `CanSaveToDisk` have a save method and provide keys for saving individual objects or a list of objects.
protocol CanSaveToDisk: Codable {

/// Provide default logic for encoding this value.
static var defaultEncoder: JSONEncoder { get }

/// This key is used to save the individual object to disk. This works best by using a unique identifier.
var storageKeyForObject: String { get }

/// This key is used to save a list of these objects to disk. Any array of items conforming to `CanSaveToDisk` has the option to save as well.
static var storageKeyForListofObjects: String { get }

/// Persists the object to disk.
///
/// - Throws: useful to throw an error from an encoder or a custom error if you use stage different from user defaults like the keychain
func save() throws

}

Using protocol extensions we add an option to save an array of these objects.

extension Array where Element: CanSaveToDisk {

func dataValue() throws -> Data {
return try Element.defaultEncoder.encode(self)
}

func save() throws {
let storage = UserDefaults.standard
storage.set(try dataValue(), forKey: Element.storageKeyForListofObjects)
}

}

We extend our patient object so it can know what to do when saving.

I use "storage" so that this could be swapped with NSKeychain. If you are saving sensitive data (like patient information) you should be using the keychain instead of UserDefaults. Also, make sure you comply with security and privacy best practices for health data in whatever market you're offering your app. Laws can be a very different experience between countries. UserDefaults might not be safe enough storage.

There are lots of great keychain wrappers to make things easier. UserDefaults simply sets data using a key. The Keychain does the same. A wrapper like https://github.com/evgenyneu/keychain-swift will behave similar to how I use UserDefaults below. I have commented out what the equivalent use would look like for completeness.

extension Patient: CanSaveToDisk {

static var defaultEncoder: JSONEncoder {
let encoder = JSONEncoder()
// add additional customization here
// like dates or data handling
return encoder
}

var storageKeyForObject: String {
// "com.myapp.patient.123"
return "com.myapp.patient.\(number)"
}

static var storageKeyForListofObjects: String {
return "com.myapp.patientList"
}

func save() throws {

// you could also save to the keychain easily
//let keychain = KeychainSwift()
//keychain.set(dataObject, forKey: storageKeyForObject)

let data = try Patient.defaultEncoder.encode(self)
let storage = UserDefaults.standard
storage.setValue(data, forKey: storageKeyForObject)
}
}

Saving is simplified, check out the 2 examples below!

do {

// saving just one patient record
// this saves this patient to the storageKeyForObject
try patientList.first?.save()

// saving the entire list
try patientList.save()


} catch { print(error) }

UserDefaults does not store custom Swift Class properties

You've saved the custom class successfully with its properties. The mistake you are doing here is you are retrieving the stored value and printing the newly created instance. Don't use the same name myTest for both instances. Store the retrieved value in myTest

if let testData = UserDefaults.standard.data(forKey: "myTest"),
let storedMyTest = try? JSONDecoder().decode(Test.self, from: testData) {
self.myTest = storedMyTest
}

To save the testedValues in UserDefaults automatically whenever it is changed use computed variable like this

class Test: Codable {
var testedValues: Double {
didSet {
//save in UserDefaults
if let encoded = try? JSONEncoder().encode(self) {
UserDefaults.standard.set(encoded, forKey: "myTest")
}
}
}
init () {
testedValues = 0.0
}
}

How do I save a custom class in swift to NSUserDefaults?

From the documentation:

The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary.

You'll want to look at using NSKeyedArchiver instead for custom objects.

Save Dictionary of custom objects in Swift using UserDefaults

You need to make your class NSCoding compliant and inherit from NSObject. Change your Contact declaration to class Contact: NSObject, NSCoding {. And Btw conditionally cast to force unwrap it later is pointless. decoder.decodeObject(forKey: "name") as! String

class Contact: NSObject, NSCoding {
var name = String()
var phone = String()
init(name: String, phone: String){
self.name=name
self.phone=phone
}
required init(coder decoder: NSCoder){
self.name = decoder.decodeObject(forKey: "name") as! String
self.phone = decoder.decodeObject(forKey: "phone") as! String
}
func encode(with coder: NSCoder){
coder.encode(name, forKey: "name")
coder.encode(phone, forKey: "phone")
}
}

Testing:

let contactDictionary = ["A":[Contact(name: "Annabel",phone: "000")]]

let encodedData = NSKeyedArchiver.archivedData(withRootObject: contactDictionary)
UserDefaults.standard.set(encodedData, forKey: "contactDictionary")

if let data = UserDefaults.standard.data(forKey: "contactDictionary") {
print("yep")
let contactDictionary2 = NSKeyedUnarchiver.unarchiveObject(with: data) as! [String : [Contact]]
}
else{
print("nope")
}

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

Store custom class in UserDefaults

Thank you Tj3n. JSON also need to conform NSCoding. Just Transactions isn't enough. I created different variables for string and integers in the Transactions NSObject, extracted data from JSON, stored extracted data in the variables declared and then encoded and decoded them accordingly. It worked.

If there is another way, I'm all ears.



Related Topics



Leave a reply



Submit