How to save class in swift to userdefaults without arguments - ios

I was unable to find a suitable community to post in, so I am sorry if this is off-topic. I am having trouble saving a custom class in swift to userdefaults. Every other answer I have seen requires initializing the class with arguments but I am looking for a way around that when encoding. I also wonder if userdefaults is the best choice? It is a large amount of data but I am trying to avoid using a relational database because I am just trying to save this data structure directly without creating a schema. It produces an error when adding mediations to the mediation object array and then trying to encode the data.
My code:
import Foundation
class SavedData: NSObject, NSCoding {
func encode(with aCoder: NSCoder) {
aCoder.encode(mediations, forKey: "mediations")
aCoder.encode(name, forKey: "name")
}
required convenience init?(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObject(forKey: "name") as! String
let mediations = aDecoder.decodeObject(forKey: "mediations") as! [Mediation]
self.init(name: name, mediations: mediations)
}
init(name: String, mediations: [Mediation]) {
// Get Saved Mediations from memory
self.mediations = mediations
self.name = name
}
public var mediations: [Mediation]
var name: String = "foo"
class Mediation {
init(name: String, role: String, data: [[String]]) {
self.name = name
self.data = Data(defendant: data[0], plaintiff: data[1])
}
var role: String = ""
var name: String = ""
var data: Data
class Data {
init(defendant: [String], plaintiff: [String]) {
self.defendant = defendant
self.plaintiff = plaintiff
}
var plaintiff: [String] = []
var defendant: [String] = []
}
}
func new_mediation (name: String, role: String, data: [[String]]) {
let mediation = Mediation(name: name, role: role, data: data)
self.mediations.append(mediation)
}
}

My favourite way to save array of custom class to UserDefaults is to use JSONEncoder.
So make sure your Mediation is Codable by:
Class Mediation: Codable
Then to save the array:
let encodedMediations = try! JSONEncoder().encode(mediations)
UserDefaults.standard.set(encodedMediations, forKey: "SavedMediations")
Finally to fetch the array:
if let savedMediations = UserDefaults.standard.data(forKey: "SavedMediations") {
let decodedMediations = try! JSONDecoder().decode([Mediation].self, from: savedMediations)
}

Related

How to use UserDefaults.standard save custom Data?

My Data Class
import Foundation
class People {
let peopleImage : String
let peopleTime : Int
let peopleName : String
init(image:String, second:Int, name:String) {
peopleImage = image
peopleTime = second
peopleName = name
}
My Data List File
import Foundation
class CustomPeopleList {
var peopleList = [
People(image: "Man", second: 12, name: "Andy"),
People(image: "Woman", second: 60, name: "Kevin"),
]
}
my viewController :
let defaults = UserDefaults.standard
var allPeopleList = CustomPeopleList
There is a button, when I click button it will delete the first item in the Data List, but I find it always error. my userdefault code is this:
self.allPeopleList.remove(at: indexPathTimer.row)
let aaa = self.allPeopleList
let newPeopleData = NSKeyedArchiver.archivedData(withRootObject: self.allPeopleList)
self.defaults.set(aaa, forKey: "myPeopleData")
and when i want to use it
if let peopleData = defaults.data(forKey: "myPeopleData") as? [People] {
allPeopleList = peopleData
}
var allPeopleList = NSKeyedUnarchiver.unarchiveObject(with: peopleData!) as? [Peoples]
the xcode say it wrong
If you're using NSKeyedArchiver and NSKeyedUnarchiver, then the objects you archive must subclass NSObject and conform to NSCoding.
You'd have to do something like this:
class People: NSObject, NSCoding {
let peopleImage : String
let peopleTime : Int
let peopleName : String
init(image:String, second:Int, name:String) {
peopleImage = image
peopleTime = second
peopleName = name
super.init()
}
required init?(coder aDecoder: NSCoder) {
self.peopleImage = aDecoder.decodeObject(forKey: "peopleImage") as! String
self.peopleTime = aDecoder.decodeInteger(forKey: "peopleTime")
self.peopleName = aDecoder.decodeObject(forKey: "peopleName") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.peopleImage, forKey: "peopleImage")
aCoder.encode(self.peopleTime, forKey: "peopleTime")
aCoder.encode(self.peopleName, forKey: "peopleName")
}
}
class CustomPeopleList: NSObject, NSCoding {
var peopleList = [
People(image: "Man", second: 12, name: "Andy"),
People(image: "Woman", second: 60, name: "Kevin"),
]
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
self.peopleList = aDecoder.decodeObject(forKey: "peopleList") as! [People]
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.peopleList, forKey: "peopleList")
}
}
var allPeopleList = CustomPeopleList()
let data = NSKeyedArchiver.archivedData(withRootObject: allPeopleList)
However
Implementing NSCoding can be very verbose.
If your objects include basic entities, like integers, strings, and arrays/dictionaries of encodable entities, then it may be easier to use Swift's new Codable protocol.
The advantage of this method is that if your objects are simple, then Swift can generate the encode and decode methods for you.
I personally recommend Codable. It can be much simpler than the old NSCoding method.
That would look like this:
class People: Codable {
let peopleImage : String
let peopleTime : Int
let peopleName : String
init(image:String, second:Int, name:String) {
peopleImage = image
peopleTime = second
peopleName = name
}
}
class CustomPeopleList: Codable {
var peopleList = [
People(image: "Man", second: 12, name: "Andy"),
People(image: "Woman", second: 60, name: "Kevin"),
]
}
var allPeopleList = CustomPeopleList()
// Can save in whatever format you want. JSON is always light and simple.
let data = try JSONEncoder().encode(allPeopleList)
// Decode the data object later.
let decodedPeopleList = try JSONDecoder().decode(CustomPeopleList.self, from: data)
I recommend the Codable protocol and to save the data as JSON. It's swiftier than Obj-C related NSKeyed(Un)Archiver
Adopt the protocol
class People : Codable {
Encode the array as JSON and save it
do {
let newPeopleData = try JSONEncoder().encode(self.allPeopleList)
self.defaults.set(newPeopleData, forKey: "myPeopleData")
} catch { print(error)
To read the data is very simple, too
do {
if let newPeopleData = self.defaults.data(forKey: "myPeopleData") {
allPeopleList = try JSONDecoder().decode([People].self, from: newPeopleData)
}
} catch { print(error)
Note: I'd name the class in singular form Person because an array of People ([People]) is tautologic and to name the properties image, time and name.
You need to inherit People class from NSObject when you are using NSKeyedUnarchiver or NSKeyedArchiver.
To store custom object in userdefaaults you need to inherit custom class form NSObject otherwise you will get runtime error/crashes.
class People: NSObject {
}

Save custom object to NSUserDefaults

I want to save my object into NSUserDefaults.
here is my Model:
class User: SafeJson {
var email: String?
var name: String?
}
SafeJson:
class SafeJson: NSObject {
override func setValue(_ value: Any?, forKey key: String) {
let uppercasedFirstCharacter = String(key.first!).uppercased()
let range = NSMakeRange(0, 1)
let selectorString = NSString(string: key).replacingCharacters(in: range, with: uppercasedFirstCharacter)
let selector = NSSelectorFromString("set\(selectorString):")
let responds = self.responds(to: selector)
if !responds {
print("\n\n\n*******--->\(selector) key is missing in API response...<---*******\n\n\n")
return
}
super.setValue(value, forKey: key)
}
}
Created UserObj by this:
let UserObj = User()
UserObj.setValuesForKeys(responseOfAPI)
now I want to save UserObj to NSUserDefaults but don't know how to do...
and I don't want to use required init(coder aDecoder: NSCoder) and func encode(with aCoder: NSCoder) in my model class because there are so many properties of User Model.
thanks!!
You cannot serialize objects to UserDefaults, generally.
However, since you're working with JSON, you can use the Codable type to translate the object to/from JSON for storage.
[*] One caveat is that Any isn't Codable. The simplest way to make a type codable is to declare its properties using types that are already Codable. These types include standard library types like String, Int, and Double; and Foundation types like Date, Data, and URL. Any type whose properties are codable automatically conforms to Codable just by declaring that conformance.
Here's a simplified example:
//: Playground - noun: a place where people can play
import UIKit
class SafeJson: Codable {
var strValues:[String:String] = [:]
var intValues:[String:Int] = [:]
func setValue(_ value: String?, forKey key: String) {
strValues[key] = value
}
func setValue(_ value: Int?, forKey key: String) {
intValues[key] = value
}
}
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let sampleObject = SafeJson()
sampleObject.setValue(12, forKey: "intValue1")
sampleObject.setValue(12345, forKey: "intValue2")
sampleObject.setValue("Banana", forKey: "yellowFruit1")
sampleObject.setValue("Orange", forKey: "orangeFruit1")
let encoded = try encoder.encode(sampleObject)
let key = "encodedObj"
UserDefaults.standard.setValue(encoded, forKey: key)
if let readValue = UserDefaults.standard.value(forKey: key) as? Data {
let decodedObj = try decoder.decode(SafeJson.self, from: readValue)
print("Decoded to \(decodedObj)")
}

fatal error: unexpectedly found nil while unwrapping an Optional value in Swift 3

this Struct is work in swift 2
I have a Swift 3 struct like this.
let tempContacts = NSMutableArray()
let arrayOfArray = NSMutableArray()
I have encode The Person Object in this for loop
for person in tempContacts as! [Person] {
let encodedObject: Data = NSKeyedArchiver.archivedData(withRootObject: person) as Data
arrayOfArray.add(encodedObject)
}
I have decode the data in this for loop
let tempContacts2 = NSMutableArray()
for data in arrayOfArray {
let person: Person = NSKeyedUnarchiver.unarchiveObject(with: data as! Data) as! Person
tempContacts2.add(person)
}
but unarchiveObject is always return nil value
First your model class should conform to the NSCoder protocol. The rest is really simple, there's no need to store the archived results for each object in an array, you can pass the initial array directly to NSKeyedArchiver like this :
class Person: NSObject, NSCoding {
var name = ""
init(name: String) {
self.name = name
}
// NSCoder
required convenience init?(coder decoder: NSCoder) {
guard let name = decoder.decodeObject(forKey: "name") as? String else { return nil }
self.init(name: name)
}
func encode(with coder: NSCoder) {
coder.encode(self.name, forKey: "name")
}
}
let tempContacts = [Person(name: "John"), Person(name: "Mary")]
let encodedObjects = NSKeyedArchiver.archivedData(withRootObject: tempContacts)
let decodedObjects = NSKeyedUnarchiver.unarchiveObject(with: encodedObjects)
As a side note : if NSCoder compliance is correctly implemented in your model class, you can of course use your way of archiving/unarchiving individual objects too. So your original code works too, with some minor adjustments:
for person in tempContacts {
let encodedObject = NSKeyedArchiver.archivedData(withRootObject: person)
arrayOfArray.add(encodedObject)
}
var tempContacts2 = [Person]()
for data in arrayOfArray {
let person: Person = NSKeyedUnarchiver.unarchiveObject(with: data as! Data) as! Person
tempContacts2.append(person)
}
Note 2: if you absolutely wants to use NSMutableArrays that's possible too, just define tempContacts like this:
let tempContacts = NSMutableArray(array: [Person(name: "John"), Person(name: "Mary")])
The rest is working without changes.
Note 3: The reason it used to work in Swift 2 and it's not working anymore in Swift 3 is that the signature for the NSCoder method func encode(with coder:) changed in Swift 3.

iOS: Custom object's int variable returns nil in NSUserDefaults

I have this weird issue. I'm saving custom class object in NSUserDefaults, and while retrieving the data I get nil for int variable of the object. Below is the custom class
class User {
var name: String?
var user_id: Int?
var account_id: Int?
var location: String?
}
I'm saving the object as,
let defaults = NSUserDefaults.standardUserDefaults()
var data = NSKeyedArchiver.archivedDataWithRootObject([user]) // I can see the int values for the user objects here
defaults.setObject(data, forKey: "all_users")
Retrieving the data as,
let defaults = NSUserDefaults.standardUserDefaults()
let data = defaults.dataForKey("all_users")
var users = [Users]()
if data != nil {
let userData = NSKeyedUnarchiver.unarchiveObjectWithData(data!) as! [Users]
for usr in userData {
print("\(usr.name!)") // Prints the name
print("\(usr.user_id!)") // Nil value here
users.append(usr)
}
}
I have absolutely no idea about the reason for this behavior.
Custom classes that have none property list items need to conform to NSCoding to be able to be saved in NSUserDefaults.
Here is a guide to conforming to NSCoding: http://nshipster.com/nscoding/
You will need both of these functions:
init(coder decoder: NSCoder) {
self.name = decoder.decodeObjectForKey("name") as String
self.user_id = decoder.decodeIntegerForKey("user_id")
self.account_id = decoder.decodeIntegerForKey("account_id")
self.location = decoder.decodeObjectForKey("self.location") as String
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.name, forKey: "name")
coder.encodeInt(self.user_id, forKey: "user_id")
coder.encodeInt(account_id, forKey: "account_id")
coder.encodeObject(self.location, forKey: "location")
}

NSKeyedUnarchiver won't return data

I am making an app that tracks a user's workouts. I have two custom classes, the first being ExerciseModel, which holds the data for each exercise performed during the workout, including the name, sets, reps, etc. Here is my data model:
import UIKit
class ExerciseModel: NSObject, NSCoding
{
// MARK: Properties
var name: String
var sets: Int
var reps: Int
var heartrate: Int?
var type: String?
//MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("exercises")
// MARK: Initialization
init?(name: String, sets: Int, reps: Int, heartrate: Int?, type: String)
{
// MARK: Initlaize stored properties
self.name = name
self.sets = sets
self.reps = reps
self.heartrate = heartrate
self.type = type
super.init()
// Initialization should fail if there is no name or sets is negative
if name.isEmpty || sets < 0
{
return nil
}
}
struct PropertyKey
{
static let nameKey = "name"
static let setKey = "sets"
static let repKey = "reps"
static let heartrateKey = "heartrate"
static let typekey = "type"
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder)
{
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeInteger(sets, forKey: PropertyKey.setKey)
aCoder.encodeInteger(reps, forKey: PropertyKey.repKey)
aCoder.encodeObject(type, forKey: PropertyKey.typekey)
}
required convenience init?(coder aDecoder: NSCoder)
{
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let sets = aDecoder.decodeIntegerForKey(PropertyKey.setKey)
let reps = aDecoder.decodeIntegerForKey(PropertyKey.repKey)
let heartrate = aDecoder.decodeIntegerForKey(PropertyKey.heartrateKey)
let type = aDecoder.decodeObjectForKey(PropertyKey.typekey) as? String
// Must call designated initializer
self.init(name: name, sets: sets, reps: reps, heartrate: heartrate, type: type!)
}
init?(name: String, sets: Int, reps: Int, heartrate: Int, type: String)
{
// Initialize stored properties.
self.name = name
self.sets = sets
self.reps = reps
self.heartrate = heartrate
self.type = type
}
}
My second custom class is called WorkoutStorage, and this is meant to allow the user to save entire workouts and retrieve them later. The exercise property is an array of ExerciseModel objects, described above. Here is my data model for WorkoutStorage:
//
import UIKit
#objc(WorkoutStorage)
class WorkoutStorage: NSObject, NSCoding
{
// MARK: Properties
var name: String
var date: NSDate
var exercises: [ExerciseModel]
var maxHR: Int
var avgHR: Int
// MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("storedWorkouts")
// MARK: Initialization
init?(name: String, date: NSDate, exercises: [ExerciseModel], maxHR: Int, avgHR: Int)
{
//MARK: Initialize Stored Properties
self.name = name
self.date = date
self.exercises = exercises
self.maxHR = maxHR
self.avgHR = avgHR
super.init()
}
struct PropertyKey
{
static let nameKey = "name"
static let dateKey = "date"
static let exercisesKey = "exercises"
static let maxHRKey = "maxHR"
static let avgHRKey = "avgHR"
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder)
{
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(date, forKey: PropertyKey.dateKey)
aCoder.encodeObject(exercises, forKey: PropertyKey.exercisesKey)
aCoder.encodeInteger(maxHR, forKey: PropertyKey.maxHRKey)
aCoder.encodeInteger(avgHR, forKey: PropertyKey.avgHRKey)
}
required convenience init?(coder aDecoder: NSCoder)
{
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let date = aDecoder.decodeObjectForKey(PropertyKey.dateKey) as! NSDate
let exercises = aDecoder.decodeObjectForKey(PropertyKey.exercisesKey) as! [ExerciseModel]
let maxHR = aDecoder.decodeIntegerForKey(PropertyKey.maxHRKey)
let avgHR = aDecoder.decodeIntegerForKey(PropertyKey.avgHRKey)
// Must call designated initializer
self.init(name: name, date: date, exercises: exercises, maxHR: maxHR, avgHR: avgHR)
}
}
I followed the Apple tutorial for Persist Data to set up NSKeyedArchiver and NSKeyedUnarchiver for this, but I am still having trouble retrieving my data. When I try to load the Workouts, I call the following function:
func loadStoredWorkouts() -> WorkoutStorage
{
NSKeyedUnarchiver.setClass(WorkoutStorage.self, forClassName: "WorkoutStorage")
NSKeyedArchiver.setClassName("WorkoutStorage", forClass: WorkoutStorage.self)
print("\(WorkoutStorage.ArchiveURL.path!)")
return NSKeyedUnarchiver.unarchiveObjectWithFile(WorkoutStorage.ArchiveURL.path!) as! WorkoutStorage
}
Currently I can only return a single WorkoutStorage object, but when I attempt to retrieve an array containing all the stored WorkoutStorage objects, I get an error saying: Could not cast value of type 'Workout_Tracker.WorkoutStorage' (0x1000fcc80) to 'NSArray' (0x19f6b2418). I have read a lot of documentation trying to figure out why this will only return a single object, as well as checked out questions with similar issues, but to no avail. I originally set up my app following the Apple Persist Data tutorial to store and load my ExerciseModel objects, and that seems to work flawlessly. I set up the WorkoutStorage class the same way, but there seems to be an issue here.
Any help would be greatly appreciated!!
**Edit*
Here is the code I use to archive the WorkoutStorage object:
func saveWorkoutStorageObject(currentWorkout: WorkoutStorage)
{
NSKeyedUnarchiver.setClass(WorkoutStorage.self, forClassName: "WorkoutStorage")
NSKeyedArchiver.setClassName("WorkoutStorage", forClass: WorkoutStorage.self)
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(currentWorkout, toFile: WorkoutStorage.ArchiveURL.path!)
if !isSuccessfulSave
{
print("Failed to save exercises")
}
if isSuccessfulSave
{
print("Successful save of current workout: \(currentWorkout)")
}
}
Workouts are only created one at a time by the user, so each time one is completed, I pass the object to the above function to archive it.
To unarchive all the objects, I was trying to do something along the lines of:
var workouts = [WorkoutStorage]()
override func viewDidLoad()
{
super.viewDidLoad()
workouts = loadStoredWorkouts()
}
where the loadStoredWorkouts() function would be:
func loadStoredWorkouts() -> [WorkoutStorage]
{
NSKeyedUnarchiver.setClass(WorkoutStorage.self, forClassName: "WorkoutStorage")
NSKeyedArchiver.setClassName("WorkoutStorage", forClass: WorkoutStorage.self)
print("\(WorkoutStorage.ArchiveURL.path!)")
return NSKeyedUnarchiver.unarchiveObjectWithFile(WorkoutStorage.ArchiveURL.path!) as! [WorkoutStorage]
}
Your saveWorkoutStorageObject only archives a single workout. It doesn't archive the array, so of course you can't unarchive an array.
You need to archive the workouts array if you want to be able to unarchive an array.
Each time you archive something to a file you replace the contents of the file. It doesn't append to the end.
Since NSKeyedArchiver.archiveRootObject automatically archives child objects, all you need to do is archive the array and your WorkoutStorage objects will be archived automagically
func saveWorkouts(workouts:[WorkoutStorage])
{
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(workouts, toFile: WorkoutStorage.ArchiveURL.path!)
if isSuccessfulSave
{
print("Successful save of workouts: \(workouts)")
} else {
print("Failed to save exercises")
}
}

Resources