How to set a class instance to userdefaults in swift? - iphone

I researched it, but I couldnt find the right solution. I want to set a class instance to userDefaults. Assume that I have a class like this:
class Person {
var id: Int
var name: String
var email: String
init() {
self.id = 0
self.name = ""
self.email = ""
}
}
I created an instance from person class, after WebService call finished and I did:
var person: Person = Person()
person.id = personJSON.valueForKey(WSConstants.USER_ID) as Int
person.name = personJSON.valueForKey(WSConstants.USER_NAME) as String
person.email = personJSON.valueForKey(WSConstants.USER_EMAIL) as String
and then, I actually want to do this:
var userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setObject(person, forKey: "personInfo")
userDefaults.synchronize()
but I it is wrong, what is the right way to set a class instance to userDefaults ?
Thanks for your answers,
Best regards

As per NSUserDefaults Class Reference:
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
You need to serialize your Person instance into one of those objects before saving it into the user defaults.
One of many ways to do it is to implement NSCoding protocol in your class:
class Person: NSCoding {
required init(coder aDecoder: NSCoder) {
self.id = aDecoder.decodeIntegerForKey("id")
self.name = aDecoder.decodeStringForKey("name")
self.email = aDecoder.decodeStringForKey("email")
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeInteger(self.id, forKey: "id")
aCoder.encodeString(self.name, forKey: "name")
aCoder.encodeString(self.email, forKey: "email")
}
}
// when writing your defaults...
let personData = NSKeyedArchiver.archivedDataWithRootObject(person)
userDefaults.setObject(personData, forKey: "personInfo")
// then, when reading your defaults...
let personData = userDefaults.objectForKey("personInfo") as NSData?
if let personData = personData? {
let person = NSKeyedUnarchiver.unarchiveObjectWithData(personData) as Person
}
Alternatively, you can just save your personJSON dictionary to the defaults.

Related

How to encode/decode a dictionary with Codable values for storage in UserDefaults?

I am trying to store a dictionary of company names (string) mapped to Company objects (from a struct Company) in iOS UserDefaults. I have created the Company struct and made it conform to Codable. I have one example a friend helped me with in my project where we created a class Account and stored it in UserDefaults by making a Defaults struct (will include example code). I have read in the swift docs that dictionaries conform to Codable and in order to stay Codable, must contain Codable objects. That is why I made struct Company conform to Codable.
I have created a struct for Company that conforms to Codable. I have tried using model code to create a new struct CompanyDefaults to handle the getting and setting of the Company dictionary from/to UserDefaults. I feel I have some beginner misconceptions about what needs to happen and about how it should be implemented (with good design in mind).
The dictionary I wish to store looks like [String:Company]
where company name will be String and a Company object for Company
I used conform to Codable as I did some research and it seemed like a newer method for completing similar tasks.
Company struct
struct Company: Codable {
var name:String?
var initials:String? = nil
var logoURL:URL? = nil
var brandColor:String? = nil // Change to UIColor
enum CodingKeys:String, CodingKey {
case name = "name"
case initials = "initials"
case logoURL = "logoURL"
case brandColor = "brandColor"
}
init(name:String?, initials:String?, logoURL:URL?, brandColor:String?) {
self.name = name
self.initials = initials
self.logoURL = logoURL
self.brandColor = brandColor
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
initials = try values.decode(String.self, forKey: .initials)
logoURL = try values.decode(URL.self, forKey: .logoURL)
brandColor = try values.decode(String.self, forKey: .brandColor)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(initials, forKey: .initials)
try container.encode(logoURL, forKey: .logoURL)
try container.encode(brandColor, forKey: .brandColor)
}
}
Defaults struct to control storage
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
let companies = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [String : Company] ?? [:]
return companies!
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: companies, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}
I should be able to reference the stored dictionary throughout my code like CompanyDefaults.companies.count
For reference, a friend helped me do a similar task for an array of Account classes stored in user defaults. The code that works perfectly for that is below. The reason I tried a different way is that I had a different data structure (dictionary) and made the decision to use structs.
class Account: NSObject, NSCoding {
let service: String
var username: String
var password: String
func encode(with aCoder: NSCoder) {
aCoder.encode(service)
aCoder.encode(username)
aCoder.encode(password)
}
required init?(coder aDecoder: NSCoder) {
guard let service = aDecoder.decodeObject() as? String,
var username = aDecoder.decodeObject() as? String,
var password = aDecoder.decodeObject() as? String else {
return nil
}
self.service = service
self.username = username
self.password = password
}
init(service: String, username: String, password: String) {
self.service = service
self.username = username
self.password = password
}
}
struct Defaults {
static private let accountsKey = "accountsKey"
static var accounts: [Account] = {
guard let data = UserDefaults.standard.data(forKey: accountsKey) else { return [] }
let accounts = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Account] ?? []
return accounts
}() {
didSet {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: accounts, requiringSecureCoding: false) else {
return
}
UserDefaults.standard.set(data, forKey: accountsKey)
}
}
}
You are mixing up NSCoding and Codable. The former requires a subclass of NSObject, the latter can encode the structs and classes directly with JSONEncoder or ProperListEncoder without any Keyedarchiver which also belongs to NSCoding.
Your struct can be reduced to
struct Company: Codable {
var name : String
var initials : String
var logoURL : URL?
var brandColor : String?
}
That's all, the CodingKeys and the other methods are synthesized. I would at least declare name and initials as non-optional.
To read and save the data is pretty straightforward. The corresponding CompanyDefaults struct is
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
return try? JSONDecoder.decode([String:Company].self, from: data) ?? [:]
}() {
didSet {
guard let data = try? JSONEncoder().encode(companies) else { return }
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}

Use Codable to serialize custom class containing reference loop

I am trying to serialize a custom class containing a reference loop and got it working using NSCoding:
import Foundation
class Person: NSObject, NSCoding {
let name: String
weak var parent: Person?
var child: Person?
init(name: String) {
self.name = name
}
required init(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
self.parent = aDecoder.decodeObject(forKey: "parent") as? Person
self.child = aDecoder.decodeObject(forKey: "child") as? Person
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(parent, forKey: "parent")
aCoder.encode(child, forKey: "child")
}
}
let per = Person(name: "Per")
let linda = Person(name: "Linda")
linda.child = per
per.parent = linda
var people = [Person]()
people.append(linda)
people.append(per)
let encodedData = NSKeyedArchiver.archivedData(withRootObject: people)
let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: encodedData) as! [Person]
myPeopleList.forEach({
print("\($0.name)\n\t Child: \($0.child?.name ?? "nil")\n\t Parent: \($0.parent?.name ?? "nil")"
)})
// Linda
// Child: Per
// Parent: nil
// Per
// Child: nil
// Parent: Linda
No I want to do the same using Codable:
import Foundation
class Person: Codable {
let name: String
weak var parent: Person?
var child: Person?
init(name: String) {
self.name = name
}
}
let per = Person(name: "Per")
let linda = Person(name: "Linda")
linda.child = per
per.parent = linda
var people = [Person]()
people.append(linda)
people.append(per)
let archiver = NSKeyedArchiver()
try archiver.encodeEncodable(people, forKey: NSKeyedArchiveRootObjectKey)
But I get the error:
error: Execution was interrupted, reason: EXC_BAD_ACCESS
During the last line. I assume it has to do with the reference loop, because it works if I comment out the line:
per.parent = linda
So can we use Codable to serialize reference loops? If so, how?
You can choose which properties are serialized by overriding Coding Keys (from here)
e.g. in your case within the class:
enum CodingKeys: String, CodingKey
{
case name
case child
}
Only the keys included here should be saved, so no child->parent loop. This does however mean the connection will only exist in one direction when loading, so you will have to re-connect them when loaded.
FWIW, if you're dealing with 2-way relationships you will be much better off using a database rather than using NSKeyedArchiver for persistence.

How do you save a custom class as an attribute of a CoreData entity in Swift 3?

I have a CoreData Entity SavedWorkout. It has the following attributes:
completionCounter is an array of Bool, and workout is a custom class called Workout.
I am saving my data like so:
let saveCompletionCounter = currentCompletionCounter
let saveDate = Date() as NSDate
let saveRoutineIndex = Int16(currentWorkoutRoutine)
let saveWorkout = NSKeyedArchiver.archivedData(withRootObject: workout)
item.setValue(saveDate, forKey: "date")
item.setValue(saveWorkout, forKey: "workout")
item.setValue(saveRoutineIndex, forKey: "routineIndex")
item.setValue(saveCompletionCounter, forKey: "completionCounter")
do {
try moc.save()
print("save successful")
} catch {
print("saving error")
}
where moc is an instance of NSManagedObjectContext, and item is an instance of NSManagedObject:
moc = appDelegate.managedObjectContext
entity = NSEntityDescription.entity(forEntityName: "SavedWorkout", in: moc)!
item = NSManagedObject(entity: entity, insertInto: moc)
In accordance with this and this and this , I have made my Workout class conform to NSObject and NSCoding, so it now looks like this:
class Workout: NSObject, NSCoding {
let name: String
let imageName: String
let routine: [WorkoutRoutine]
let shortDescription: String
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String
imageName = aDecoder.decodeObject(forKey: "imageName") as! String
routine = aDecoder.decodeObject(forKey: "routine") as! [WorkoutRoutine]
shortDescription = aDecoder.decodeObject(forKey: "shortDescription") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(imageName, forKey: "imageName")
aCoder.encode(routine, forKey: "routine")
aCoder.encode(shortDescription, forKey: "shortDescription")
}
init(name: String, imageName: String, routine: [WorkoutRoutine], shortDescription: String) {
self.name = name
self.imageName = imageName
self.routine = routine
self.shortDescription = shortDescription
}
}
However I always get an error on the line routine: aDecoder.decodeObject....
The error says:
NSForwarding: warning: object 0x60800002cbe0 of class 'App.WorkoutRoutine' does not implement methodSignatureForSelector: -- trouble ahead
Unrecognized selector -[FitLift.WorkoutRoutine replacementObjectForKeyedArchiver:]
Why does this give me an error and not the other Transformable attribute? How do I save a custom class as a property of a CoreData entity?
The issue is that WorkoutRoutine is itself a custom class and as of your error it is not NSCoding compliant, therefore aCoder.encode(routine, forKey: "routine") doesn't really know how to encode it, as well as routine = aDecoder.decodeObject(forKey: "routine") as! [WorkoutRoutine] doesn't know how to decode it.
Not really related, but please try a safer approach for your coder and encoder initializer as the force unwrap might cause crashes if the encoder does not contain the keys you are looking for (for any reason)
required init?(coder aDecoder: NSCoder) {
guard let name = aDecoder.decodeObject(forKey: "name") as? String,
let imageName = aDecoder.decodeObject(forKey: "imageName") as? String,
let routine = aDecoder.decodeObject(forKey: "routine") as? [WorkoutRoutine],
let shortDescription = aDecoder.decodeObject(forKey: "shortDescription") as? String else {
return nil
}
self.name = name
self.imageName = imageName
self.routine = routine
self.shortDescription = shortDescription
}

Swift 3 optional parameters

Is it possible to create optional initialization parameters in Swift so I can create an object from JSON with the values returned from the API call, but then when I'm saving that object later I can also save the downloaded UIImage for one of the urls I got before.
Example:
class Story: NSObject, NSCoding {
var id: Int?
var title, coverImageURL: String?
var coverImage: UIImage?
required init?(anId: Int?, aTitle: String?, aCoverImageURL: String?) {
self.id = anId
self.title = aTitle
self.coverImageURL = aCoverImageURL
}
convenience init?(json: [String: Any]) {
let id = json["id"] as? Int
let title = json["title"] as? String
let coverImageURL = json["cover_image"] as? String
self.init(
anId: id,
aTitle: title,
aCoverImageURL: coverImageURL,
)
}
Then Later I want to save objects to memory
//MARK: Types
struct PropertyKey {
static let id = "id"
static let title = "title"
static let coverImageURL = "coverImageURL"
static let coverImage = "coverImage"
}
//MARK: NSCoding
func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: PropertyKey.id)
aCoder.encode(title, forKey: PropertyKey.title)
aCoder.encode(coverImageURL, forKey: PropertyKey.coverImageURL)
aCoder.encode(coverImage, forKey: PropertyKey.coverImage)
}
required convenience init?(coder aDecoder: NSCoder) {
guard let id = aDecoder.decodeObject(forKey: PropertyKey.id) as? Int else {
os_log("Unable to decode the id for a Story object.", log: OSLog.default, type: .debug)
return nil
}
guard let title = aDecoder.decodeObject(forKey: PropertyKey.title) as? String else {
os_log("Unable to decode the title for a Story object.", log: OSLog.default, type: .debug)
return nil
}
let coverImageURL = aDecoder.decodeObject(forKey: PropertyKey.coverImageURL) as? String
let coverImage = aDecoder.decodeObject(forKey: PropertyKey.coverImage) as? UIImage
self.init(
anId: id,
aTitle: title,
aCoverImageURL: coverImageURL,
coverImage: coverImage,
)
}
Does this make sense? I want to be able to save a Story object as soon as I get the response from the API, but later when I save the story to memory, I want to be able to save the fetched UIImage for the coverImage.
How would I do that?
I'm not sure why no one took the easy points on this answer, but the answer is to simply make your properties optionals, and then you can set them with a value, or nil. You can also create convenience initializers that automatically set certain values to nil if you want. So, using my app as an example, I have a model that gets built from an API call. that model has values like id, created_at, etc that don't exist until a record is saved to the server, but I create objects locally, store them, and eventually send them to the server, so I need to be able to set the above values only when creating an object from JSON, so here is what I did:
class Story: NSObject, NSCoding {
var id: Int?
var title, coverImageURL: String?
var coverImage: UIImage?
required init?(anId: Int?, aTitle: String?, aCoverImageURL: String?) {
self.id = anId
self.title = aTitle
self.coverImageURL = aCoverImageURL
}
convenience init?(json: [String: Any]) {
let id = json["id"] as? Int
let title = json["title"] as? String
let coverImageURL = json["cover_image"] as? String
self.init(
anId: id,
aTitle: title,
aCoverImageURL: coverImageURL,
)
}
convenience init?(aTitle: String, aCoverImage: UIImage?) {
let title = aTitle
let subtitle = aSubtitle
let coverImage = aCoverImage
let isActive = activeStatus
self.init(
anId: nil,
aTitle: title,
aCoverImageURL: nil,
aCoverImage: coverImage,
)
}
As you can see, I only set two of the values when I'm creating an object locally, and the other values are just set to nil. To allow a value to be set to nil, just make it an optional when setting it. Simple!

Swift objects array to plist file

I am trying to save my object's array to array.plist but I get the following error:
Thread 1: signal SIGABRT error
My object class looks like this:
class Note {
// MARK: Properties
var title: String
var photo: UIImage?
var text: String
// MARK: Initialization
init?(title: String, photo: UIImage?, text: String) {
// Initialize stored properties.
self.title = title
self.photo = photo
self.text = text
// Initialization should fail if there is no name or if the rating is negative.
if title.isEmpty{
return nil
}
}
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeObject(title, forKey:"title")
aCoder.encodeObject(text, forKey:"text")
aCoder.encodeObject(photo, forKey:"photo")
}
init (coder aDecoder: NSCoder!) {
self.title = aDecoder.decodeObjectForKey("title") as! String
self.text = aDecoder.decodeObjectForKey("text") as! String
self.photo = aDecoder.decodeObjectForKey("photo") as! UIImage
}
}
In the controller, I try to save the array with the Notes object like this:
notes = [Notes]()
notes.append(note)
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.AllDomainsMask, true)
let path: AnyObject = paths[0]
let arrPath = path.stringByAppendingString("/array.plist")
NSKeyedArchiver.archiveRootObject(notes, toFile: arrPath)
Not all the properties in your class are not optional, yet when you retrieve them from the plist, you are unwrapping all of them. This might cause your code to crash.
For example, if the photo is nil and you saved the object, when you are retrieving it, you are unwrapping it self.photo = aDecoder.decodeObjectForKey("photo") as! UIImage, which will crash if you did not save anything there.
Try removing the unwrapping and check again for your crash. Even if this was not the cause of your crash, it will cause a crash at some point.
If this does not fix your problem, please paste the complete error log so it is a bit more clear what is happening.
For swift 5. You can save an array of custom classes to a .plist file that inherits from NSObject and NSSecureCoding.
If we create a custom class called Person:
import Foundation
class Person: NSObject, NSSecureCoding {
//Must conform to NSSecureCoding protocol
public class var supportsSecureCoding: Bool { return true } //set to 'true'
//just some generic things to describe a person
private var name:String!
private var gender:String!
private var height:Double!
//used to create a new instance of the class 'Person'
init(name:String, gender:String, height:Double) {
super.init()
self.name = name
self.gender = gender
self.height = height
}
//used for NSSecureCoding:
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name") //encodes the name to a key of 'name'
coder.encode(gender, forKey: "gender")
coder.encode(height, forKey: "height")
}
//used for NSSecureCoding:
required init?(coder: NSCoder) {
super.init()
self.name = (coder.decodeObject(forKey: "name") as! String)
self.gender = (coder.decodeObject(forKey: "gender") as! String)
self.height = (coder.decodeObject(forKey: "height") as! Double)
}
//created just to print the data from the class
public override var description: String { return String(format: "name=%#,gender=%#,height%f", name, gender, height) }
}
Now we can create functions to save and load from a .plist file in the ViewController class:
We need to gather data from the directory system of the device:
func documentsDirectory()->String {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths.first!
return documentsDirectory
}
func dataFilePath ()->String{
return self.documentsDirectory().appendingFormat("/your_file_name_here.plist")
}
function to save the array:
func saveData(_ people:[Person]) {
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
archiver.encode(people, forKey: "your_file_name_here")
let data = archiver.encodedData
try! data.write(to: URL(fileURLWithPath: dataFilePath()))
}
function to load the array:
func loadData() -> [Person] {
let path = self.dataFilePath()
let defaultManager = FileManager()
var arr = [Person]()
if defaultManager.fileExists(atPath: path) {
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
let unarchiver = try! NSKeyedUnarchiver(forReadingFrom: data)
//Ensure the unarchiver is required to use secure coding
unarchiver.requiresSecureCoding = true
//This is where it is important to specify classes that can be decoded:
unarchiver.setClass(Person.classForCoder(), forClassName: "parentModule.Person")
let allowedClasses =[NSArray.classForCoder(),Person.classForCoder()]
//Finally decode the object as an array of your custom class
arr = unarchiver.decodeObject(of: allowedClasses, forKey: "your_file_name_here") as! [Person]
unarchiver.finishDecoding()
}
return arr
}
In the ViewController class:
override func viewDidLoad() {
super.viewDidLoad()
let testPerson = Person(name: "Bill", gender: "Male", height: 65.5)
let people:[Person] = [testPerson]
//Save the array
saveData(people)
//Load and print the first index in the array
print(loadData()[0].description)
}
Output:
[name=Bill,gender=Male,height=65.5000000]