How to save array of objects (with image variables) in Swift and Xcode? - swift

I am wondering how to save an array of objects from the following class:
class CustomDocument: NSObject, NSCoding {
let name : String
let image : UIImage
init(n: String, i: UIImage){
name = n
image = i
}
//other code excluded
}
Originally, I saved this array to User Defaults. Because the objects took up a lot of space, it caused a lot of lag in the app.
What is the best way to save an array of data that takes up a lot of space?
Thank you so much for the help and all responses are appreciated.

Try this code, Hope it helps:
class CustomDocument: NSObject, NSCoding {
var name : String?
var image : UIImage?
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "namekey")
if let imageData = image!.jpegData(compressionQuality: 1.0){
aCoder.encode(imageData, forKey: "imagekey")
}
UserDefaults.standard.synchronize()
}
required convenience init?(coder aDecoder: NSCoder) {
self.init()
if let name = (aDecoder.decodeObject(forKey: "namekey") as? String){
self.name = name
}
if let imageData = (aDecoder.decodeObject(forKey: "imagekey") as? Data){
if let image = UIImage(data: imageData){
self.image = image
}
}
}
}
func archiveDocument(document:CustomDocument) -> Data? {
do {
let archivedObject = try NSKeyedArchiver.archivedData(withRootObject: document, requiringSecureCoding: false)
return archivedObject
} catch {
// do something with the error
}
return nil
}
func unarchiveDocument(unarchivedObject:Data) -> CustomDocument? {
do {
if let document = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) as? CustomDocument {
return document
}
} catch {
// do something with the error
}
return nil
}
Example:
//Set the object, also you can use an array instead of an object
let obj = CustomDocument()
obj.name = "doc1"
obj.image = UIImage(named: "my_image")
if let archivedObject = archiveDocument(document: obj){
UserDefaults.standard.set(archivedObject, forKey: "obj")
}
//Get the object
if let archivedObject = UserDefaults.standard.data(forKey: "obj"){
obj = unarchiveDocument(unarchivedObject: archivedObject)
let myImage = obj?.image
}

Related

NSKeyedUnarchiver seems not to be reading anything

I'm trying to write an array of objects using NSKeyedArchiver.
Here some parts from my code:
EventStore.swift - holding the event array:
class EventStore{
private var events: [EventItem] = [EventItem]()
static let sharedStore = EventStore()
private init() {
}
static func getEventFile() -> URL{
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let file = directory.appendingPathComponent("events.bin")
return file
}
func addEvent(withEvent event:EventItem){
events.append(event)
}
func getEvents()->[EventItem]{
return events
}
}
No the eventItem where I implemented NSCoding:
class EventItem: NSObject, NSCoding {
private var id:Int
private var timestamp:Int64
//Object initialization
init(withId id:Int,withTimestamp timestamp:Int64) {
self.id = id
self.timestamp = timestamp
}
required convenience init?(coder: NSCoder) {
//get value from stored key if exists
guard let id = coder.decodeObject(forKey: "id") as? Int,
let timestamp = coder.decodeObject(forKey: "timestamp") as? Int64
//exit init after decoding if a value is missing
else {
NSLog("Unable to decode event")
return nil
}
self.init(withId:id,withTimestamp:timestamp)
}
func getId()->Int{
return id
}
func getTimestamp()->Int64{
return timestamp
}
//encode values to keys
func encode(with aCoder: NSCoder) {
NSLog("Encoding event")
aCoder.encode(id, forKey: "id")
aCoder.encode(timestamp, forKey: "timestamp")
}
}
Finally when the user tape on a button I'm adding an event into the array and saving it:
var eventStore = EventStore.sharedStore
#IBAction func TakeAction() {
//generate new event
let timestamp = Int64(NSDate().timeIntervalSince1970 * 1000)
let newEvent = EventItem(withId: eventStore.eventCount(), withTimestamp: timestamp)
eventStore.addEvent(withEvent: newEvent)
saveEvents()
//refresh ui
updateTakeText()
}
func saveEvents(){
do{
let data = try NSKeyedArchiver.archivedData(withRootObject: eventStore.getEvents(), requiringSecureCoding: false)
NSLog("Data being written : \(data)")
try data.write(to: EventStore.getEventFile())
NSLog("Write events to file :\(EventStore.getEventFile())")
}catch{
NSLog(error.localizedDescription)
}
}
func loadEvents() {
do{
let data = try Data(contentsOf: EventStore.getEventFile())
NSLog("Data loaded from file path: \(data)")
//try get data else return empty array
let events = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [EventItem] ?? [EventItem]()
NSLog("Events retrived from file: \(events.count)")
eventStore.setEvents(withEvents:events)
}catch{
NSLog(error.localizedDescription)
}
}
I added a lot of debug and it seems that the encoding and file write are working fine but the decoding fail. It always get nil values.
Any clue?
Thanks in advance
When encoding Int values you have to decode them with coder.decodeInteger(forKey: "xxx")

Custom Class Unarchive is nil in Cocoa Swift

I am trying to save and retrieve a custom class to UserDefaults in my macOS app. I am getting nil for newData
class countClass: NSObject, NSCoding {
var leftClickCount : Int = 0
init(leftClickCount: Int) {
self.leftClickCount = leftClickCount
super.init()
}
func encode(with coder: NSCoder) {
coder.encode(self.leftClickCount, forKey: "leftClickCount")
}
required convenience init?(coder decoder: NSCoder) {
guard let leftClickCount = decoder.decodeObject(forKey: "leftClickCount") as? Int
else {
return nil
}
self.init(
leftClickCount: leftClickCount
)
}
}
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let leftC = countClass(leftClickCount: 25)
let ud = UserDefaults.standard
let archivedData = NSKeyedArchiver.archivedData(withRootObject: leftC)
ud.set(archivedData, forKey: "data")
ud.synchronize()
let tempData = ud.object(forKey: "data") as! Data
let newData = NSKeyedUnarchiver.unarchiveObject(with: tempData) as! countClass // Getting nil here
}
}
I was able to fix this problem by changing from:
decoder.decodeObject(forKey: "leftClickCount") as? Int
with:
decoder.decodeInteger(forKey: "leftClickCount")

Why is retrieval from NSUserDefaults failing for my custom object?

I have a class PredicPair which inherits from NSCoding and NSObject as such:
class PredicPair: NSObject, NSCoding {
var weight : Float
var prediction : String
init(weight: Float, prediction: String) {
self.weight = weight
self.prediction = prediction
super.init()
}
func encode(with aCoder: NSCoder) {
aCoder.encode(weight, forKey: "weight")
aCoder.encode(prediction, forKey: "prediction")
}
required convenience init(coder aDecoder: NSCoder) {
let unarchivedWeight = aDecoder.decodeObject(forKey: "weight") as! Float
let unarchivedPrediction = aDecoder.decodeObject(forKey: "prediction") as! String
self.init(weight: unarchivedWeight, prediction: unarchivedPrediction)
}
class func saveToUserDefaults(pairs: [PredicPair]) {
let dataBlob = NSKeyedArchiver.archivedData(withRootObject: pairs)
UserDefaults.standard.set(dataBlob, forKey: "test")
UserDefaults.standard.synchronize()
}
class func loadFromUserDefaults() -> [PredicPair]? {
guard let decodedNSDataBlob = UserDefaults.standard.object(forKey: "test") as? NSData,
let loadedFromUserDefault = NSKeyedUnarchiver.unarchiveObject(with: decodedNSDataBlob as Data) as? [PredicPair]
else {
return nil
}
return loadedFromUserDefault
}
}
I am trying to store an array of the class in UserDefaults and retrieving it, but the latter always returns nil. Any reason why?
let predicPair1 = PredicPair(weight: 0, prediction: "there")
let predicPair2 = PredicPair(weight: 1, prediction: "hello")
let array = [predicPair1, predicPair2]
PredicPair.saveToUserDefaults(pairs: array)
if let retreivedArray = PredicPair.loadFromUserDefaults() {
print("loaded \(retreivedArray.count) players from NSUserDefaults")
} else {
print("failed")
}
I've also tried saving to a file using NSKeyedArchiver but retrieval fails as well.

[NSKeyedUnarchiver init]: cannot use -init for initialization' error when trying to save a custom class

I get the error:
[NSKeyedUnarchiver init]: cannot use -init for initialization
when trying to load a custom class.
Heres my Init for the class:
required init?(coder aDecoder: NSCoder) {
print("intializing")
if let quoteName = aDecoder.decodeObjectForKey("quoteName") as? String {
self._quoteName = quoteName
}
if let quote = aDecoder.decodeObjectForKey("quote") as? String {
self._quote = quote
}
if let soundFileName = aDecoder.decodeObjectForKey("soundFileName") as? String {
self._soundFileName = soundFileName
}
if let soundFileType = aDecoder.decodeObjectForKey("soundFileType") as? String {
self._soundFileType = soundFileType
}
if let audioFilePath = aDecoder.decodeObjectForKey("audioFilePath") as? String {
self._soundFileType = audioFilePath
}
if let state = aDecoder.decodeObjectForKey("state") as? Bool {
self._state = state
}
if let number = aDecoder.decodeObjectForKey("number") as? Int {
self._number = number
}
}
Heres my Encoder function
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self._quoteName, forKey: "quoteName")
aCoder.encodeObject(self._quote, forKey: "quote")
aCoder.encodeObject(self._soundFileType, forKey: "soundFileName")
aCoder.encodeObject(self._soundFileType, forKey: "soundFileType")
aCoder.encodeObject(self._audioFilePath, forKey: "audioFilePath")
aCoder.encodeObject(self._state, forKey: "state")
aCoder.encodeObject(self._number, forKey: "number")
}
Any finally my call to load:
class func loadQuoteList(){
print("loading Quotes")
quoteList.removeAll()
var quoteListLength = defaults.integerForKey("quoteListLength")
//unarchives Quote Object
var unarc = NSKeyedUnarchiver()
if let data = defaults.objectForKey("Quote") as? NSData {
unarc = NSKeyedUnarchiver(forReadingWithData: data)
}
print("loading individual quotes")
for(var index = 0; index < quoteListLength; index++){
var newQuote = unarc.decodeObjectForKey("Quote\(index)") as! Quote
quoteList[index] = newQuote
print(newQuote._quoteName)
}
}

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]