I'm just trying coding in Swift and I am trying to modify this existing project in the Apple Dev Library re: Meals.
I was hoping to put in an additional subclass such as Ingredients into the main Meal class having it as an array or ingredients.
import UIKit
import os.log
class Meal: NSObject, NSCoding {
//MARK: Properties
var name: String
var photo: UIImage?
var rating: Int
var recipe: [ingredients]?
//MARK: Archiving Paths
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("meals")
//MARK: Types
struct PropertyKey {
static let name = "name"
static let photo = "photo"
static let rating = "rating"
static let recipe = "recipe"
}
//MARK: Initialization
init?(name: String, photo: UIImage?, rating: Int, recipe: ingredients!) {
// The name must not be empty
guard !name.isEmpty else {
return nil
}
// The rating must be between 0 and 5 inclusively
guard (rating >= 0) && (rating <= 5) else {
return nil
}
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || rating < 0 {
return nil
}
// Initialize stored properties.
self.name = name
self.photo = photo
self.rating = rating
self.recipe = recipe
}
//MARK: NSCoding
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: PropertyKey.name)
aCoder.encode(photo, forKey: PropertyKey.photo)
aCoder.encode(rating, forKey: PropertyKey.rating)
aCoder.encode(recipe, forKey: PropertyKey.recipe)
}
required convenience init?(coder aDecoder: NSCoder) {
// The name is required. If we cannot decode a name string, the initializer should fail.
guard let name = aDecoder.decodeObject(forKey: PropertyKey.name) as? String else {
os_log("Unable to decode the name for a Meal object.", log: OSLog.default, type: .debug)
return nil
}
// Because photo is an optional property of Meal, just use conditional cast.
let photo = aDecoder.decodeObject(forKey: PropertyKey.photo) as? UIImage
let rating = aDecoder.decodeInteger(forKey: PropertyKey.rating)
let recipe = aDecoder.decodeObject(forKey: PropertyKey.recipe)
// Must call designated initializer.
self.init(name: name, photo: photo, rating: rating, recipe: recipe)
}
}
Function calls for saving and loading the Meal items are as follows:
private func saveMeals() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path)
if isSuccessfulSave {
os_log("Meals successfully saved.", log: OSLog.default, type: .debug)
} else {
os_log("Failed to save meals...", log: OSLog.default, type: .error)
}
}
private func loadMeals() -> [Meal]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Meal.ArchiveURL.path) as? [Meal]
}
I declared a new Class Ingredients.swift to capture the ingredients that I wanted.
import UIKit
import os.log
class Ingredients: NSObject {
struct PropertyKey {
static let name = "name"
static let quantity = "quantity"
}
var name: String!
var quantity: Double!
//MARK: Initialization
init?(name: Int, quantity: Double) {
self.name = name
self.quantity = quantity
}
}
The problem I am having now is that the XCode is throwing a "Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '-[MealTracker.Ingredients encodeWithCoder:]: unrecognized selector sent to instance
0x1c40a0e40'"
Can I please know how to successfully include the Ingredients array into the saved object?
You have to implement NSCoding protocol
func encode(with aCoder: NSCoder) {}
required convenience init?(coder aDecoder: NSCoder) {}
Inside the inner custom classes also
class Ingredients: NSObject , NSCoding {}
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")
}
}
I am trying to save store a custom file using UserDefaults, but my app crashesh due to :
fatal error: unexpectedly found nil while unwrapping an Optional value
Here is my code , in my custom class I declare an empty array
class AppDefualts: NSObject {
var downloadedURLArray = [DownloadFile]() //Declare an empty array
override init() {
super.init()
downloadedURLArray = loadStoredURL()
}
//Store data
func addStored(file:DownloadFile) {
//Add URL to array and save it
downloadedURLArray.append(file)
let data = NSKeyedArchiver.archivedData(withRootObject: downloadedURLArray)
UserDefaults.standard.set(data, forKey: "storedURL")
}
//Load:
func loadStoredURL() -> Array<DownloadFile> {
let data = UserDefaults.standard.data(forKey: "storedURL")
downloadedURLArray = NSKeyedUnarchiver.unarchiveObject(with: data!) as? [DownloadFile] ?? [DownloadFile]()
return downloadedURLArray
}
Any help would be great
EDIT 1 :
I added NSCoding protocols :
func encode(with aCoder: NSCoder) {
aCoder.encode(downloadedURLArray, forKey: "storedURL")
}
required init?(coder aDecoder: NSCoder) {
downloadedURLArray = aDecoder.decodeObject(forKey: "storedURL") as! [DownloadFile]
}
now app crashes due to this :
* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '* -encodeObject:forKey:
cannot be sent to an abstract object of class NSCoder: Create a
concrete instance!'
If you want to store the custom object in UserDefault then you have to do Encoding/Decoding. Your custom object suppose to confirm to the NSCoding Protocol.
Store into UserDefault : You have to convert you object in to NSData and store them into UserDefault by using NSKeyedArchiver
Get from UserDefault : Get the NSData from USerDefault and convert into Custom object by using NSKeyedUnArchiver
Refer the link to convert custom object into NSData and vice versa
Example: custom object which confirms NSCoding protocol
class Book: NSObject, NSCoding {
var title: String
var author: String
var pageCount: Int
var categories: [String]
var available: Bool
// Memberwise initializer
init(title: String, author: String, pageCount: Int, categories: [String], available: Bool) {
self.title = title
self.author = author
self.pageCount = pageCount
self.categories = categories
self.available = available
}
// MARK: NSCoding
required convenience init?(coder decoder: NSCoder) {
guard let title = decoder.decodeObjectForKey("title") as? String,
let author = decoder.decodeObjectForKey("author") as? String,
let categories = decoder.decodeObjectForKey("categories") as? [String]
else { return nil }
self.init(
title: title,
author: author,
pageCount: decoder.decodeIntegerForKey("pageCount"),
categories: categories,
available: decoder.decodeBoolForKey("available")
)
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.title, forKey: "title")
coder.encodeObject(self.author, forKey: "author")
coder.encodeInt(Int32(self.pageCount), forKey: "pageCount")
coder.encodeObject(self.categories, forKey: "categories")
coder.encodeBool(self.available, forKey: "available")
}
}
import UIKit
class Event: NSObject, NSCoding {
// MARK: Properties
var name: String
var created_at: String
var stands:[Stand?]
// MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("events")
// MARK: Types
struct PropertyKey {
static let nameKey = "name"
static let createdAtKey = "created_at"
static let standsAtKey = "stands"
}
// MARK: Initialization
init?(name: String, created_at: String, stands:[Stand?]) {
// Initialize stored properties.
self.name = name
self.created_at = created_at
self.stands = stands
super.init()
// Initialization should fail if there is no name or if no created_at.
if name.isEmpty || created_at.isEmpty {
return nil
}
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(created_at, forKey: PropertyKey.createdAtKey)
aCoder.encodeObject(stands, forKey: PropertyKey.standsAtKey)
}
required convenience init?(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let created_at = aDecoder.decodeObjectForKey(PropertyKey.createdAtKey) as! String
let stands = aDecoder.decodeObjectForKey(PropertyKey.standsAtKey) as! [Stand?]
// Must call designated initializer.
self.init(name: name, created_at: created_at, stands: stands)
}
}
I have got error from "aCoder.encodeObject(stands, forKey: PropertyKey.standsAtKey)" saying "Cannot convert value of type[Stand?] to expected argument type 'AnyObject?'"
I am using NSCoding to save the object with array of empty Stand, then later retrieve it and update the Stands property of this Class (Event)
You cannot encode your custom class "Event" unless your class "Stand" is also NSCoding compliant.
In Swift 4 you can conform to the protocol Codable which does the encoding en decoding
class Test: Codable {
}
For more information, read https://developer.apple.com/documentation/swift/codable
I am trying to use NSUserDefaults to save an array in to my app's core data. I thought it would be good to use NSUserDefaults but the problem is that wherever I put the code that creates the default it throws up the SIGABRT error.
Here is the code that creates the default:
let levelArrayDefault = NSUserDefaults.standardUserDefaults()
levelArrayDefault.setValue(levelsArray, forKey: "levelsArray")
levelArrayDefault.synchronize()
levelsArray is an array of List objects:
class List: NSObject, NSCoding {
// MARK: Properties
var name: String
var AnswersArray = [Answer]()
init?(name: String) {
// Initialize stored properties.
self.name = name
if name.isEmpty {
return nil
}
}
required init(coder decoder: NSCoder){
self.AnswersArray = (decoder.decodeObjectForKey("AA") as? [Answer])!
self.name = (decoder.decodeObjectForKey("name") as? String)!
}
func encodeWithCoder(coder: NSCoder) {
if let AnswersArray = AnswersArray { coder.encodeObject(AnswersArray, forKey: "AA") }
if let name = name { coder.encodeObject(name, forKey: "name") }
}
}
class Answer: NSObject, NSCoding {
var EnglishAnswer: String = ""
var ChineseAnswer: String = ""
init(newEng: String, newChi: String){
self.EnglishAnswer = newEng
self.ChineseAnswer = newChi
}
required init(coder decoder: NSCoder){
self.EnglishAnswer = (decoder.decodeObjectForKey("EnglishAnswer") as? String)!
self.ChineseAnswer = (decoder.decodeObjectForKey("ChineseAnswer") as? String)!
}
func encodeWithCoder(coder: NSCoder) {
if let EnglishAnswer = EnglishAnswer { coder.encodeObject(EnglishAnswer, forKey: "EnglishAnswer") }
if let ChineseAnswer = ChineseAnswer { coder.encodeObject(ChineseAnswer, forKey: "ChineseAnswer") }
}
}
How can I stop SIGABRT from popping up and get the array to be stored.
Help would be much appreciated.
You need to convert it to NSData using NSKeyedArchiver before storing it to NSUserDefaults, try like this:
update: Xcode 11.4 • Swift 5.2 or later
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let list = List(name: "Student")
list.answers = [Answer(english: "english answer", chinese: "中文回答")]
let data = (try? NSKeyedArchiver.archivedData(withRootObject: [list], requiringSecureCoding: false)) ?? Data()
UserDefaults.standard.set(data, forKey: "listData")
guard
let loadedData = UserDefaults.standard.data(forKey: "listData"),
let loadedArray = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(loadedData) as? [List]
else { return }
print(loadedData.count)
print(loadedArray.first ?? "none")
print(loadedArray.first?.name ?? "no name")
print(loadedArray.first?.answers.first?.english ?? "no english")
print(loadedArray.first?.answers.first?.chinese ?? "no chinese")
}
}
class Answer: NSObject, NSCoding {
let english: String
let chinese: String
init(english: String, chinese: String) {
self.english = english
self.chinese = chinese
}
required init(coder decoder: NSCoder) {
self.english = decoder.decodeString(forKey: "english")
self.chinese = decoder.decodeString(forKey: "chinese")
}
func encode(with coder: NSCoder) {
coder.encode(english, forKey: "english")
coder.encode(chinese, forKey: "chinese")
}
}
class List: NSObject, NSCoding {
let name: String
fileprivate var data = Data()
var answers: [Answer] {
get {
(try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? [Answer] ?? []
}
set {
data = (try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false)) ?? Data()
}
}
init(name: String) {
self.name = name
}
required init(coder decoder: NSCoder) {
self.data = decoder.decodeData(forKey: "answersData")
self.name = decoder.decodeString(forKey: "name")
}
func encode(with coder: NSCoder) {
coder.encode(data, forKey: "answersData")
coder.encode(name, forKey: "name")
}
}
extension NSCoder {
func decodeString(forKey key: String) -> String {
return decodeObject(forKey: key) as? String ?? ""
}
func decodeData(forKey key: String) -> Data {
return decodeObject(forKey: key) as? Data ?? Data()
}
}
If you want to save your custom object in NSUserDefaults, it's not enough to make your class NSCoding-compliant -- you have to actually encode the data into an NSData object. This is a common mistake -- see my answer to another question for a similar situation.
So, you've added NSCoding to your Answer and List classes. That's a good start. Before you continue, you should verify that you've got that step right by using a NSKeyedArchiver to encode an example of a List object containing a few Answer objects into an instance of NSData, and then use NSKeyedUnarchiver to decode that data object back into your List. Verify that everything that you care about completes the round trip with no problems. This would be an excellent place to use Xcode's testing facility -- you could write a unit test that does exactly what I've described.
Once you know you've got the NSCoding stuff right, you should modify your code so that it encodes your List as NSData and stores the resulting data object in NSUserDefaults using the -setObject:forKey: method.
When i'm trying to run the int(coder:) it shows this error, i don't know why ?? "Non failable initializer requirement init(coder:) cannot be satisfied by a failable initalizer ('init?')"
class Note: NSObject, NSCoding {
var name: String
var photo: UIImage?
var rating: Int
static let DocumentsDirectory: AnyObject = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("notes")
struct PropertyKey {
static let nameKey = "name"
static let photoKey = "photo"
static let ratingKey = "rating"
}
init?(name: String, photo: UIImage?, rating: Int) {
self.name = name
self.photo = photo
self.rating = rating
super.init()
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || rating < 0 {
return nil
}
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(photo, forKey: PropertyKey.photoKey)
aCoder.encodeInteger(rating, forKey: PropertyKey.ratingKey)
}
required convenience init?(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
// Because photo is an optional property of Meal, use conditional cast.
let photo = aDecoder.decodeObjectForKey(PropertyKey.photoKey) as? UIImage
let rating = aDecoder.decodeIntegerForKey(PropertyKey.ratingKey)
// Must call designated initializer.
self.init(name: name, photo: photo, rating: rating)
}
}
i'm using xcode 6 , and by the way when this code runs in xcode 7 it do not show any errors , what is the reason ?
So I found a way to make it work, the 'init(coder:)' method can't be failable so what I did was override the 'init()' method to be called by the 'init(coder:)' method because it needs to call 'self.init()'. So here is the code:
class Meal: NSObject, NSCoding {
// MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("meals")
// MARK: Properties
var name: String
var rating: Int
var photo: UIImage?
// MARK: Types
struct PropertyKey {
static let nameKey = "name"
static let photoKey = "photo"
static let ratingKey = "rating"
}
// MARK: Initialization
init? (name: String, rating: Int, photo: UIImage?) {
// Intialize stored properties.
self.name = name
self.rating = rating
self.photo = photo
super.init()
// Initialization should fail if there is no name or if the rating is negative.
if self.name.isEmpty || (rating < 0) {
return nil
}
}
override private init () {
self.name = ""
self.rating = 0
self.photo = nil
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(self.photo, forKey: PropertyKey.photoKey)
aCoder.encodeObject(self.rating, forKey: PropertyKey.ratingKey)
}
convenience required init(coder aDecoder: NSCoder) {
self.init()
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
if let rating = aDecoder.decodeIntegerForKey(PropertyKey.ratingKey) {
self.rating = rating
}
// Because photo is an optional property of Meal, use conditional cast.
let photo = aDecoder.decodeObjectForKey(PropertyKey.photoKey) as? UIImage
self.name = name
self.photo = photo
}
}
I made the 'init()' method private so that only methods inside the class can call it.
I also had to optional unwrap the rating because the app was crashing complaining about not being able to unarchive Int with the ratingKey.
Speculation on my part as to specifics, but, generally - the precise syntax and semantics of Swift changed significantly between Xcode 6 (say, Xcode 6.4) and the new Xcode 7 betas. Xcode 6.4 supports / uses Swift language version 1.2; Xcode 7 uses Swift language version 2.0 (beta)
Found a solution to run above code in xcode6 , should remove "convenience" from the init(coder aDecoder) and use super.init() instead of self.init(name: name, photo: photo, rating: rating) then it works perfectly
class Note: NSObject, NSCoding {
var name: String
var photo: UIImage?
var rating: Int
struct PropertyKey {
static let nameKey = "name"
static let photoKey = "photo"
static let ratingKey = "rating"
}
init(name: String, photo: UIImage?, rating: Int) {
self.name = name
self.photo = photo
self.rating = rating
super.init()
// Initialization should fail if there is no name or if the rating is negative.
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(photo, forKey: PropertyKey.photoKey)
aCoder.encodeInteger(rating, forKey: PropertyKey.ratingKey)
}
required init(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
// Because photo is an optional property of Meal, use conditional cast.
self.photo = aDecoder.decodeObjectForKey(PropertyKey.photoKey) as? UIImage
self.rating = aDecoder.decodeIntegerForKey(PropertyKey.ratingKey) as Int
// Must call designated initializer.
super.init()
}
}