I have two classes : Contact and Bill. Contact has an array of type Bill.
When I persist using NSKeyedArchiver my contacts persist just fine, however, it does not persist the Bill array.
Each time I add a a Bill or a Contact I call the insertNewObject() method in persist.
Here are my classes :
/* Persist.swift */
import Foundation
class Persist {
static let sharedInstance = Persist()
let delegate = UIApplication.sharedApplication().delegate as! AppDelegate
var contactsFilePath : String {
let manager = NSFileManager.defaultManager()
let url = manager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first! as NSURL
return url.URLByAppendingPathComponent("\(delegate.userId):objectsArrayz").path!
}
func insertNewObject(){
NSKeyedArchiver.archiveRootObject(delegate.contacts!, toFile: contactsFilePath)
}
func retrieveContracts(){
if let array = NSKeyedUnarchiver.unarchiveObjectWithFile(contactsFilePath) as? [Contact] {
delegate.contacts = array
}
}
func deleteContact(rowNum : Int){
delegate.contacts!.removeAtIndex(rowNum)
NSKeyedArchiver.archiveRootObject(delegate.contacts!, toFile: contactsFilePath)
}
/* Contact.swift */
import Foundation
class Contact : NSObject, NSCoding{
var image : UIImage?
var firstName : String?
var lastName : String?
var email : String?
var phoneNumber : String?
var address : String?
var bills : [Bill]?
init(image: UIImage, firstName : String, lastName : String, email : String, phoneNumber : String, address : String, bills : [Bill]) {
self.image = image
self.firstName = firstName
self.lastName = lastName
self.email = email
self.phoneNumber = phoneNumber
self.address = address
self.bills = bills
}
// MARK: NSCoding
required convenience init?(coder decoder: NSCoder) {
guard let image = decoder.decodeObjectForKey("image") as? UIImage,
let firstName = decoder.decodeObjectForKey("firstName") as? String,
let lastName = decoder.decodeObjectForKey("lastName") as? String,
let email = decoder.decodeObjectForKey("email") as? String,
let phoneNumber = decoder.decodeObjectForKey("phoneNumber") as? String,
let address = decoder.decodeObjectForKey("address") as? String,
let bills = decoder.decodeObjectForKey("bills") as? [Bill]
else {
return nil
}
self.init(
image: image,
firstName: firstName,
lastName : lastName,
email : email,
phoneNumber : phoneNumber,
address : address,
bills : bills
)
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.image, forKey: "image")
coder.encodeObject(self.firstName, forKey: "firstName")
coder.encodeObject(self.lastName, forKey: "lastName")
coder.encodeObject(self.email, forKey: "email")
coder.encodeObject(self.phoneNumber, forKey: "phoneNumber")
coder.encodeObject(self.address, forKey: "address")
coder.encodeObject(self.bills, forKey: "bills")
}
}
/* Bill.swift*/
import Foundation
class Bill : NSObject, NSCoding{
var service : String?
var subtotal : Double?
var taxes : Double?
var total : Double?
init(service : String, subtotal : Double, taxes : Double, total: Double) {
self.service = service
self.subtotal = subtotal
self.taxes = taxes
}
// MARK: NSCoding
required convenience init?(coder decoder: NSCoder) {
guard let service = decoder.decodeObjectForKey("service") as? String,
let subtotal = decoder.decodeObjectForKey("subtotal") as? Double,
let taxes = decoder.decodeObjectForKey("taxes") as? Double,
let total = decoder.decodeObjectForKey("total") as? Double
else {
return nil
}
self.init(
service: service,
subtotal: subtotal,
taxes: taxes,
total: total
)
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.service, forKey: "service")
coder.encodeObject(self.subtotal, forKey: "subtotal")
coder.encodeObject(self.taxes, forKey: "taxes")
coder.encodeObject(self.total, forKey: "total")
}
}
Here is a guide from the apple dev docs (in objective-c):
Discussion You must return self from initWithCoder:. If you have an
advanced need that requires substituting a different object after
decoding, you can do so in awakeAfterUsingCoder:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Articles/codingobjects.html
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
// Implementation continues
}
- (id)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_firstName = [coder decodeObjectForKey:ASCPersonFirstName];
_lastName = [coder decodeObjectForKey:ASCPersonLastName];
_height = [coder decodeFloatForKey:ASCPersonHeight];
}
return self;
}
A call to super.init() is required in the serializable class's init method.
Snippet from Bill.swift
required init?(coder decoder: NSCoder) {
super.init()
service = decoder.decodeObjectForKey("service") as? String
subtotal = decoder.decodeObjectForKey("subtotal") as? Double
taxes = decoder.decodeObjectForKey("taxes") as? Double
total = decoder.decodeObjectForKey("total") as? Double
services = decoder.decodeObjectForKey("services") as? [Service]
}
Related
I am trying to save the custom object that includes another custom object to UserDefault but I keep getting
'unrecognized selector sent to instance'
error.
I have implemented the NSCoding protocol to both the classes.
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: details)
I am trying to archive RideDetails Class and save it to UserDefault.
I have done similar in another project which didn't include nested custom object and it worked fine. May be because of the nested object it didn't work. I couldn't figure it out.
DriverProfile Class
class DriverProfile: NSObject, NSCoding{
private var userId: Int
private var driverId:Int
private var firstName: String
private var lastName: String
private var phone: String
private var email:String?
private var location: CLLocation?
private var vehicleType: String
private var vehicleNo: String
private var rating:Double?
private var imageName:String?
init(userId: Int, driverId: Int, firstName:String, lastName: String, phone:String, email:String?, location:CLLocation?,vehicleType:String,vehicleNo:String,rating:Double?,imageName:String?) {
self.userId = userId
self.driverId = driverId
self.firstName = firstName
self.lastName = lastName
self.phone = phone
self.email = email
self.location = location
self.vehicleType = vehicleType
self.vehicleNo = vehicleNo
self.rating = rating
self.imageName = imageName
}
func encode(with aCoder: NSCoder) {
aCoder.encode(userId, forKey:"userId")
aCoder.encode(driverId, forKey:"driverId")
aCoder.encode(firstName, forKey:"firstName")
aCoder.encode(lastName, forKey:"lastName")
aCoder.encode(phone, forKey:"phone")
aCoder.encode(email, forKey:"email")
aCoder.encode(location, forKey:"location")
aCoder.encode(vehicleType, forKey:"vehicleType")
aCoder.encode(vehicleNo, forKey:"vehicleNo")
aCoder.encode(rating, forKey:"rating")
aCoder.encode(imageName, forKey:"imageName")
}
required init?(coder aDecoder: NSCoder) {
userId = aDecoder.decodeInteger(forKey: "userId")
driverId = aDecoder.decodeInteger(forKey: "driverId")
firstName = aDecoder.decodeObject(forKey: "firstName") as! String
lastName = aDecoder.decodeObject(forKey: "lastName") as! String
phone = aDecoder.decodeObject(forKey: "phone") as! String
email = aDecoder.decodeObject(forKey: "email") as? String
location = aDecoder.decodeObject(forKey: "location") as? CLLocation
vehicleType = aDecoder.decodeObject(forKey: "vehicleType") as! String
vehicleNo = aDecoder.decodeObject(forKey: "vehicleNo") as! String
rating = aDecoder.decodeObject(forKey: "rating") as? Double
imageName = aDecoder.decodeObject(forKey: "imageName") as? String
super.init()
}
}
RideDetails Class
class RideDetails: NSObject{
private var rideTime:String?
private var rideId:Int
private var driverProfile:DriverProfile
private var destinationLocation: CLLocation?
private var myLocation:CLLocation?
private var otpCode:String
private var trackRideURL:String
private var customerId:Int
private var extendedRange:Bool?
private var extendedRangePrice:Double?
init(rideTime:String?,rideId:Int,driverProfile:DriverProfile,destinationLocation:CLLocation?,myLocation:CLLocation?,otpCode:String,trackRideURL:String,customerId:Int,extendedRange:Bool?,extendedRangePrice:Double?) {
self.rideTime = rideTime
self.rideId = rideId
self.driverProfile = driverProfile
self.destinationLocation = destinationLocation
self.myLocation = myLocation
self.otpCode = otpCode
self.trackRideURL = trackRideURL
self.customerId = customerId
self.extendedRange = extendedRange
self.extendedRangePrice = extendedRangePrice
}
func encode(with aCoder: NSCoder) {
aCoder.encode(rideTime, forKey:"rideTime")
aCoder.encode(rideId, forKey:"rideId")
aCoder.encode(driverProfile, forKey:"driverProfile")
aCoder.encode(destinationLocation, forKey:"destinationLocation")
aCoder.encode(myLocation, forKey:"myLocation")
aCoder.encode(otpCode, forKey:"otpCode")
aCoder.encode(trackRideURL, forKey:"trackRideURL")
aCoder.encode(customerId, forKey:"customerId")
aCoder.encode(extendedRange, forKey:"extendedRange")
aCoder.encode(extendedRangePrice, forKey:"extendedRangePrice")
}
required init?(coder aDecoder: NSCoder) {
rideTime = aDecoder.decodeObject(forKey: "rideTime") as? String
rideId = aDecoder.decodeInteger(forKey: "rideId")
driverProfile = aDecoder.decodeObject(forKey: "driverProfile") as! DriverProfile
destinationLocation = aDecoder.decodeObject(forKey: "destinationLocation") as? CLLocation
myLocation = aDecoder.decodeObject(forKey: "myLocation") as? CLLocation
otpCode = aDecoder.decodeObject(forKey: "otpCode") as! String
trackRideURL = aDecoder.decodeObject(forKey: "trackRideURL") as! String
customerId = aDecoder.decodeInteger(forKey: "customerId")
extendedRange = aDecoder.decodeObject(forKey: "extendedRange") as? Bool
extendedRangePrice = aDecoder.decodeObject(forKey: "extendedRangePrice") as? Double
super.init()
}
}
When try to encode my custom object in iOS swift get this error from Xcode 8.3
unrecognized selector sent to instance 0x60800166fe80
*** -[NSKeyedArchiver dealloc]: warning: NSKeyedArchiver deallocated without having had -finishEncoding called on it.
And my code like this:
import UIKit
import Foundation
class Place: NSObject {
func setCustomObject(CustomObject obj:Any,Key key:String) {
let encodedObject : Data = NSKeyedArchiver.archivedData(withRootObject: obj)
UserDefaults.standard.set(encodedObject, forKey: key)
}
}
Here's an example how to make an object to conform to NSCoding. Basically you need to provide implementation of two methods - required convenience init?(coder decoder: NSCoder) and encode(with aCoder: NSCoder)
class Book: NSObject, NSCoding {
var title: String?
var pageCount: Int?
// Memberwise initializer
init(title: String,pageCount: Int) {
self.title = title
self.pageCount = pageCount
}
// MARK: NSCoding
// Here you will try to initialize an object from archve using keys you did set in `encode` method.
required convenience init?(coder decoder: NSCoder) {
guard let title = decoder.decodeObject(forKey: "title") as? String else { return nil }
self.init(title: title, pageCount: decoder.decodeInteger(forKey: "pageCount"))
}
// Here you need to set properties to specific keys in archive
func encode(with aCoder: NSCoder) {
aCoder.encode(self.title, forKey: "title")
aCoder.encodeCInt(Int32(self.pageCount), forKey: "pageCount")
}
}
Also I would recommend changing your setCustomObject method to this:
func setCustomObject(obj:NSCoding, key:String) {
let encodedObject : Data = NSKeyedArchiver.archivedData(withRootObject: obj)
UserDefaults.standard.set(encodedObject, forKey: key)
}
This way compiler prevent you passing NSKeyedArchiver an object that does not conform to NSCoding protocol.
If you don't want to provide all properties in the init method you can use default values:
init(title : String? = nil, pageCount: Int? = nil){
self.title = title
self.pageCount = pageCount
}
Now you can just init your object without any properties. Like that Book()
Here is the solutions, you have to implement the two methods
Encode Method
func encode(with aCoder: NSCoder)
Decoding method
required init?(coder aDecoder: NSCoder)
Complete Example code
class User: NSObject , NSCoding
{
var userID : Int = 0
var name : String = ""
var firstName : String = ""
var lastName : String = ""
var username : String = ""
var email : String = ""
override init(){
super.init();
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.userID, forKey: "id");
aCoder.encode(self.firstName, forKey: "first_name");
aCoder.encode(self.lastName, forKey: "last_name");
aCoder.encode(self.name, forKey: "name");
aCoder.encode(self.username,forKey: "username");
aCoder.encode(self.email, forKey: "email");
}
required init?(coder aDecoder: NSCoder) {
super.init()
self.userID = aDecoder.decodeInteger(forKey: "id");
self.firstName = aDecoder.decodeObject(forKey: "first_name") as! String;
self.lastName = aDecoder.decodeObject(forKey: "last_name") as! String;
self.name = aDecoder.decodeObject(forKey: "name") as! String
self.username = aDecoder.decodeObject(forKey: "username") as! String
self.email = aDecoder.decodeObject(forKey: "email") as! String;
}
init(data : [String: AnyObject]) {
super.init()
self.userID = String.numberValue(data["user_id"]).intValue;
self.firstName = String.stringValue(data["first_name"]);
self.lastName = String.stringValue(data["last_name"]);
self.email = String.stringValue(data["email"]);
self.username = String.stringValue(data["user_name"]);
}
class func loadLoggedInUser() -> User {
if let archivedObject = UserDefaults.standard.object(forKey:"CurrentUserAcc"){
if let user = NSKeyedUnarchiver.unarchiveObject(with: (archivedObject as! NSData) as Data) as? User {
return user;
}
}
return User()
}
func saveUser(){
let archivedObject : NSData = NSKeyedArchiver.archivedData(withRootObject: self) as NSData
UserDefaults.standard.set(archivedObject, forKey: "CurrentUserAcc");
UserDefaults.standard.synchronize();
}
func deleteUser(){
UserDefaults.standard.set(nil, forKey: "CurrentUserAcc")
UserDefaults.standard.synchronize();
}
}
As a Protocol oriented programming concept, I have created my model with Struct.
I want to save Array of "Struct" into Userdefault. But I am having a problem in encode/decode of the array of this model.
Here is my model Struct
struct Room {
let name : String
let id : String
let booked : Bool
}
Here I created a extension like this
extension Room {
func decode() -> Room? {
let userClassObject = NSKeyedUnarchiver.unarchiveObject(withFile: RoomClass.path()) as? RoomClass
return userClassObject?.room
}
func encode() {
let personClassObject = RoomClass(room: self)
NSKeyedArchiver.archiveRootObject(personClassObject, toFile: RoomClass.path())
}
class RoomClass: NSObject, NSCoding {
var room : Room?
init(room: Room) {
self.room = room
super.init()
}
class func path() -> String {
let documentsPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first
let path = documentsPath?.appending(("/Room"))
return path!
}
func encode(with aCoder: NSCoder) {
aCoder.encode(room!.name, forKey: "name")
aCoder.encode(room!.id, forKey: "Id")
aCoder.encode(room!.booked, forKey: "booked")
}
required init?(coder aDecoder: NSCoder) {
let _name = aDecoder.decodeObject(forKey: "name") as? String
let _id = aDecoder.decodeObject(forKey: "Id") as? String
let _booked = aDecoder.decodeBool(forKey: "booked")
room = Room(name: _name!, id: _id!, booked: _booked)
super.init()
}
}
}
When I am trying to save arrRoomList(a Array of Room objects) like this
self.saveRooms(arrayRooms: arrRoomList)
I got this error
[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance
I have also tried to encode each object first and then try to save them in default, then also it gives an error.
Can anyone please guide me how to encode/decode the array of Struct in Userdefaults in a proper way without converting it into Dictionary?
you can setup the struct to use NSKeyedArchiver directly like this:
struct Room {
let name : String
let id : String
let booked : Bool
}
extension Room {
func encode() -> Data {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: data)
archiver.encode(name, forKey: "name")
archiver.encode(id, forKey: "id")
archiver.encode(booked, forKey: "booked")
archiver.finishEncoding()
return data as Data
}
init?(data: Data) {
let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
defer {
unarchiver.finishDecoding()
}
guard let name = unarchiver.decodeObject(forKey: "name") as? String else { return nil }
guard let id = unarchiver.decodeObject(forKey: "id") as? String else { return nil }
booked = unarchiver.decodeBool(forKey: "booked")
self.name = name
self.id = id
}
}
to use with UserDefaults, call like this:
// to encode to data and save to user defaults
let room = Room(name: "asdf", id: "123", booked: true)
UserDefaults.standard.set(room.encode(), forKey: "room")
// to retrieve from user defaults
if let data = UserDefaults.standard.object(forKey: "room") as? Data {
let room = Room(data: data)
}
Can save/retrieve an array of rooms like this:
func saveRooms(arrayRooms: [Room]) {
let roomsData = arrayRooms.map { $0.encode() }
UserDefaults.standard.set(roomsData, forKey: "rooms")
}
func getRooms() -> [Room]? {
guard let roomsData = UserDefaults.standard.object(forKey: "rooms") as? [Data] else { return nil }
return roomsData.flatMap { return Room(data: $0) }
}
// save 2 rooms to user defaults
let roomA = Room(name: "A", id: "123", booked: true)
let roomB = Room(name: "B", id: "asdf", booked: false)
saveRooms(arrayRooms: [roomA, roomB])
// get the rooms
print(getRooms())
You can try Model like
class CardModel: NSObject
{
let name : String
let id : String
let booked : Bool
override init()
{
self.name = ""
self.id = ""
self.booked = false
}
required init(coder aDecoder: NSCoder)
{
self.name = aDecoder.decodeObject(forKey: "name") as! String
self.id = aDecoder.decodeObject(forKey: "id") as! String
self.booked = aDecoder.decodeObject(forKey: "booked") as! Bool
}
func encodeWithCoder(_ aCoder: NSCoder)
{
aCoder.encode(name, forKey: "name")
aCoder.encode(id, forKey: "id")
aCoder.encode(booked, forKey: "booked")
}
}
Use by Creating CardModel model Object
let objCardModel = CardModel()
objCardModel.name = "Shrikant"
objCardModel.id = "8"
objCardModel.booked = true
Access by object
let userName = objCardModel.name
I have a class Users. And I use NSKeyedArchiver add this in NSUserDefaults, but this does not work.
This is my code:
let encodedData = NSKeyedArchiver.archivedData(withRootObject: self.user) as Data
self.prefs.setValue(encodedData, forKey: "user")
Error: libc++abi.dylib: terminating with uncaught exception of type NSException
And this is my class Users
class Users : NSObject, NSCoding {
var token : String?
var username : String?
var phone : String?
var email: String?
var date : Date?
var language : String?
var age : Int?
override init() {
}
init(token : String, username : String, phone : String, email: String, date : Date, language : String, age : Int) {
self.token = token
self.username = username
self.phone = phone
self.email = email
self.date = date
self.language = language
self.age = age
}
required init(coder aDecoder: NSCoder) {
self.token = aDecoder.decodeObject(forKey: "token") as? String
self.username = aDecoder.decodeObject(forKey: "username") as? String
self.phone = aDecoder.decodeObject(forKey: "phone") as? String
self.email = aDecoder.decodeObject(forKey: "email") as? String
self.date = aDecoder.decodeObject(forKey: "date") as? Date
self.language = aDecoder.decodeObject(forKey: "language") as? String
self.age = aDecoder.decodeInteger(forKey: "age")
}
func encode(with aCoder: NSCoder) {
aCoder.encode(token, forKey:"token")
aCoder.encode(username, forKey:"username")
aCoder.encode(phone, forKey:"phone")
aCoder.encode(date, forKey:"date")
aCoder.encode(language, forKey:"language")
aCoder.encode(age, forKey:"age")
}
}
-(void)transformObjects:(NSMutableArray*)array key:(NSString*)key
{
NSMutableArray* archiveArray = [[NSMutableArray alloc]initWithCapacity:array.count];
for (Furniture *furniture in array) {
// The error occurs on the line below
NSData *furnitureEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:furniture];
[archiveArray addObject:furnitureEncodedObject];
}
NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
[userData setObject:archiveArray forKey:key];
}
Error log:
2014-03-04 10:55:27.881 AppName[10641:60b] -[Furniture encodeWithCoder:]: unrecognized selector sent to instance 0x15d43350
I have no idea why do I get "unrecognized selector sent to instance" when trying to archive an object.
You need to implement NSCoding protocol inside your Furniture object:
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.yourpoperty forKey:#"PROPERTY_KEY"];
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if(self = [super init]){
self.yourpoperty = [aDecoder decodeObjectForKey:#"PROPERTY_KEY"];
}
return self;
}
Basically you specify what should be written (encoded) and read from a file (decoded). Usually for each property you want to store in a file, you make same as I did here in an example.
You'll need to implement NSCoding - here is an example with an object called SNStock that has two string properties, ticker and name:
import Foundation
class SNStock: NSObject, NSCoding
{
let ticker: NSString
let name: NSString
init(ticker: NSString, name: NSString)
{
self.ticker = ticker
self.name = name
}
//MARK: NSCoding
required init(coder aDecoder: NSCoder) {
self.ticker = aDecoder.decodeObjectForKey("ticker") as! NSString
self.name = aDecoder.decodeObjectForKey("name") as! NSString
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(ticker, forKey: "ticker")
aCoder.encodeObject(name, forKey: "name")
}
//MARK: NSObjectProtocol
override func isEqual(object: AnyObject?) -> Bool {
if let object = object as? SNStock {
return self.ticker == object.ticker &&
self.name == object.name
} else {
return false
}
}
override var hash: Int {
return ticker.hashValue
}
}
For Swift 4.1 (tested code)
import UIKit
import SwiftyJSON
class UserObject: NSObject, NSCoding {
var username: String? = ""
var userID: String? = ""
var user_email: String? = ""
var name: String? = ""
var age: String? = ""
var gender: String? = ""
override init() {
super.init()
}
init(dictionary: JSON) {
//User data initialize...
username = dictionary["username"].stringValue
userID = dictionary["iUserID"].stringValue
user_email = dictionary["email"].stringValue
name = dictionary["name"].stringValue
age = dictionary["age"].stringValue
gender = dictionary["gender"].stringValue
}
required public init(coder aDecoder: NSCoder) {
username = aDecoder.decodeObject(forKey: "username") as? String
userID = aDecoder.decodeObject(forKey: "iUserID") as? String
user_email = aDecoder.decodeObject(forKey: "email") as? String
name = aDecoder.decodeObject(forKey: "name") as? String
age = aDecoder.decodeObject(forKey: "age") as? String
gender = aDecoder.decodeObject(forKey: "gender") as? String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(username, forKey: "username")
aCoder.encode(userID, forKey: "iUserID")
aCoder.encode(user_email, forKey: "email")
aCoder.encode(name, forKey: "name")
aCoder.encode(age, forKey: "age")
aCoder.encode(gender, forKey: "gender")
}
}
Note : NSCoding protocol is important
You have a custom class Furniture which you are trying to archive with NSKeyedArchiver. In order for this to work, the Furniture class needs to conform to the < NSCoding > protocol. Which means implementing the encodeWithCoder: and initWithCoder: methods.
Currently you don't implement these methods. You need to add them.
I think your Furniture class does not implement the NSCoding protocol.