I have the following data architecture, where Orchestra has many Sections, and each Section has many Players :
All 3 classes conform to NSCoding protocol and have the necessary methods implemented. According to this SO question, it should work since NSCoding works recursively.
Inside the Orchestra singleton class, I have the following methods for saving and retrieving Sections:
let sectionArchivalURL: URL = {
let documentDirectories = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentDirectory = documentDirectories.first!
return documentDirectory.appendingPathComponent("sections.archive") //gets archived Player objects
} ()
func saveChanges() -> Bool {
print ("Saving sections to: \(sectionArchivalURL.path)")
return NSKeyedArchiver.archiveRootObject(allSections, toFile: sectionArchivalURL.path)
}
Section also conforms to NSCoding:
//MARK: - NSCoding methods
func encode(with aCoder: NSCoder) {
aCoder.encode(sectionName, forKey: "sectionName")
aCoder.encode(allPlayers, forKey: "allPlayers")
}
required init(coder aDecoder: NSCoder) {
sectionName = aDecoder.decodeObject(forKey: "sectionName") as! String
allPlayers = aDecoder.decodeObject(forKey: "allPlayers") as! [Player]
super.init()
}
Similarly, Player also conforms to NSCoding:
//MARK: - NSCoding methods
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "playerName")
print ("encoding Player") //does not get called
}
required init(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "playerName") as! String
super.init()
}
Saving is confirmed working. However, when the app relaunches, I am able to view my teams, but the containing Players are empty. I also know that the encoding function in Player did not get called. What am I doing incorrectly?
Leave everything the same except your Player class. Here is the way I got it working:
func encode(with aCoder: NSCoder) {
let encName = NSKeyedArchiver.archivedData(withRootObject: name)
aCoder.encode(encName, forKey: "playerName")
print ("encoding Player")
}
required init(coder aDecoder: NSCoder) {
let tmpName = aDecoder.decodeObject(forKey: "playerName")
name = NSKeyedUnarchiver.unarchiveObject(with: tmpName as! Data) as! String
}
Related
Good day,
I am on the verge of finishing the tutorial app from Code School, and I was really playing around with its archiving and unarchiving the data "to and with a file". So essentially the archiving works, which is the code below.
class func saveOrdersToArchive(cart: Orders) -> Bool {
print(archiveFilePath());
return NSKeyedArchiver.archiveRootObject(cart, toFile: archiveFilePath());
}
The the archiveFilePath() function is implemented this way, it basically creates a file called "cart.archive" and stores it on the simulator's local drive.
class func archiveFilePath() -> String {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0];
return documentsDirectory.appendingPathComponent("cart.archive").path;
}
So it creates the file correctly then stores the data of type Orders.
But when I try to retrieve the data with my implementation below, it seems that the returned data is marked as "nil".
class func readOrdersFromArchive() -> Orders? {
print(archiveFilePath());
return NSKeyedUnarchiver.unarchiveObject(withFile: archiveFilePath()) as? Orders
}
So, in the main ViewController file, the saving of the Object is implemented below.
//name
productNames = ["1907 Wall set", "1921 Dial phone"];
//cell Images
productImages = [ #imageLiteral(resourceName: "image-cell1"), #imageLiteral(resourceName: "image-cell2")];
//phone Images
phoneImages = [#imageLiteral(resourceName: "phone-fullscreen1"), #imageLiteral(resourceName: "phone-fullscreen2")];
//price
priceProducts = [1.99, 3.99]
oCartProducts = Product(names: productNames, productImages: productImages, cellImages: phoneImages, priceOfProducts: priceProducts);
order = Orders(order_id: 1, orders: oCartProducts);
print(Orders.saveOrdersToArchive(cart: order));
The function prints to true, to indicate successful archive.
The implementation for retrieving the data is implemented below,
if let order1 = Orders.readOrdersFromArchive(){
order = order1
if let o = order.orders{
if let n = o.names{
print(n.count)
}
}
}
The reason I want to print the "count" is to be able to make sure the unwrapped object has values, but the code doesn't go there meaning the object is nil.
I am doing so init in the ViewController before storing the variables as follows,
var oCartProducts = Product(names: [String](), productImages: [UIImage](), cellImages: [UIImage](), priceOfProducts: [Double]());
var order = Orders(order_id: Int(), orders: Product(names: [String](), productImages: [UIImage](), cellImages: [UIImage](), priceOfProducts: [Double]()));
Showing Orders Class,
class Orders : NSObject, NSCoding{
var order_id: Int?
var orders: Product?
init(order_id: Int?, orders: Product?){
self.order_id = order_id;
self.orders = orders;
}
required init?(coder aDecoder: NSCoder) {
self.orders = aDecoder.decodeObject(forKey: "orders") as? Product
self.order_id = aDecoder.decodeInteger(forKey: "order_id")
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.order_id);
aCoder.encode(self.orders);
}
class func archiveFilePath() -> String {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0];
return documentsDirectory.appendingPathComponent("cart.archive").path;
}
class func readOrdersFromArchive() -> Orders? {
print(archiveFilePath());
return NSKeyedUnarchiver.unarchiveObject(withFile: archiveFilePath()) as? Orders
}
class func saveOrdersToArchive(cart: Orders) -> Bool {
print(archiveFilePath());
return NSKeyedArchiver.archiveRootObject(cart, toFile: archiveFilePath());
}
Showing Product Class,
class Product: NSObject, NSCoding {
var names: [String]?
var productImages: [UIImage]?
var cellImages: [UIImage]?
var priceOfProducts: [Double]?
init(names: [String]?, productImages: [UIImage]?, cellImages: [UIImage]?, priceOfProducts: [Double]?) {
self.names = names;
self.productImages = productImages;
self.cellImages = cellImages;
self.priceOfProducts = priceOfProducts;
}
required init?(coder aDecoder: NSCoder) {
self.names = aDecoder.decodeObject(forKey: "names") as? [String];
self.productImages = aDecoder.decodeObject(forKey: "productNames") as? [UIImage];
self.cellImages = aDecoder.decodeObject(forKey: "cellImages") as? [UIImage];
self.priceOfProducts = aDecoder.decodeObject(forKey: "priceOfProducts") as? [Double];
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.names);
aCoder.encode(self.productImages);
aCoder.encode(self.cellImages);
aCoder.encode(self.priceOfProducts);
}
}
Hope you can shed some light.
Show us your Orders class. Does it conform to NSCoding?
func encode(with aCoder: NSCoder) {
if let id = self.order_id {
aCoder.encode(self.order_id, forKey: "order_id")
}
if let orders = self.orders {
aCoder.encode(self.orders, forKey: "orders")
}
}
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();
}
}
I ran into an issue where I could not use a lazy variable inside init?(coder aDecoder: NSCoder)
My sample code is
class Category: NSObject, NSCoding {
var categoryID: NSInteger!
var categoryName: String!
var categoryLogoURL: String!
lazy var categoryTags = [String]()
private override init() {
}
required init?(coder aDecoder: NSCoder) {
self.categoryID = aDecoder.decodeInteger(forKey: "categoryID")
self.categoryName = aDecoder.decodeObject(forKey: "categoryName") as! String
self.categoryLogoURL = aDecoder.decodeObject(forKey: "categoryLogoURL") as! String
self.categoryTags = aDecoder.decodeObject(forKey: "categoryTags") as! [String]
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.categoryID, forKey: "categoryID")
aCoder.encode(categoryName, forKey: "categoryName")
aCoder.encode(categoryLogoURL, forKey: "categoryLogoURL")
aCoder.encode(categoryTags, forKey: "categoryTags")
}
}
I am getting an error Use of 'self' in property access 'categoryTags' before super.init initializes self
Once I remove lazy everything works fine. What I am doing wrong?
call Super Init:
required init?(coder aDecoder: NSCoder) {
super.init()
self.categoryID = aDecoder.decodeInteger(forKey: "categoryID")
self.categoryName = aDecoder.decodeObject(forKey: "categoryName") as! String
self.categoryLogoURL = aDecoder.decodeObject(forKey: "categoryLogoURL") as! String
self.categoryTags = aDecoder.decodeObject(forKey: "categoryTags") as! [String]
}
The decodeDouble on NSCoder returns a non-optional value, but I would like to identify whether a value was nil before it was encoded.
This is my scenario:
var optionalDouble: Double? = nil
func encode(with aCoder: NSCoder) {
if let optionalDouble {
aCoder.encode(optionalDouble, forKey: "myOptionalDouble")
}
}
convenience required init?(coder aDecoder: NSCoder) {
optionalDouble = aDecoder.decodeDouble(forKey: "myOptionalDouble")
// here optionalDouble is never nil anymore
}
So decoding double returns 0 in case the value was never set, so it seems like I can't identify whether a value was actually 0 or nil before encoding
Is there a way for me to check if a double was nil before it was encoded?
The solution is to use NSNumber instead of Double when you encode, then use decodeObject to get back (if it exists) the double value. For example
class A: NSCoding {
var optionalDouble: Double? = nil
#objc func encodeWithCoder(aCoder: NSCoder) {
if let optionalDouble = optionalDouble {
aCoder.encodeObject(NSNumber(double: optionalDouble), forKey: "myOptionalDouble")
}
}
#objc required init?(coder aDecoder: NSCoder) {
if let decodedDoubleNumber = aDecoder.decodeObjectForKey("myOptionalDouble") as? NSNumber {
self.optionalDouble = decodedDoubleNumber.doubleValue
} else {
self.optionalDouble = nil
}
}
}
With suggestion from #Hamish, here is the version for Swift 3. Be aware that we need to inherit the class to NSObject in order to make NSEncoding work (Got Unrecognized selector -replacementObjectForKeyedArchiver: crash when implementing NSCoding in Swift)
class A: NSObject, NSCoding {
var optionalDouble: Double? = nil
func encode(with aCoder: NSCoder) {
if let optionalDouble = optionalDouble {
aCoder.encode(optionalDouble, forKey: "myOptionalDouble")
}
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
optionalDouble = aDecoder.decodeObject(forKey: "myOptionalDouble") as? Double
}
}
I'm trying to encode a custom class so I can save it using NSKeyedArchiver.archivedDataWithRootObject
but when I try to conform to the NSCoding protocol, I get this error : 'self' used before self.init. Here is my code:
class MemberRewardsInfo: NSObject, NSCoding {
var id: Int?
required convenience init?(coder aDecoder: NSCoder) {
guard let unarchivedId = aDecoder.decodeObjectForKey("id") as? Int
else {
return nil
}
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(id, forKey: "id")
}
}
its pretty annoying, not sure why it's not working.
The error message is sort of misleading, but you need to make init(coder:) a designated initializer.
You need to modify your init(code:) to something like this:
required init?(coder aDecoder: NSCoder) {
guard let unarchivedId = aDecoder.decodeObjectForKey("id") as? Int else {
return nil
}
self.id = unarchivedId
super.init()
}
Apparently it's upset about convenience. The assumption is that if you create a convenience initializer, it's going to chain to a "real initializer.
When using a convenience initializer, just call the designated initializer, in your case, self.init(), inside block. See this example in Apple docs for an explanation.
required convenience init?(coder aDecoder: NSCoder) {
guard let unarchivedId = aDecoder.decodeObjectForKey("id") as? Int
else {
return nil
}
self.init()
self.id = unarchivedId
}