NSKeyedArchiver.archiveRootObject returns "false" if I want to overwrite data, why? - swift

NSKeyedArchiver.archiveRootObject(<#rootObject: AnyObject#>, toFile: <#String#>)
Only returns true the first time. Every next time I call it, the method returns false.
I read some SO, some posts said that I can't rewrite data this way. However, I tried :
NSFileManager.defaultManager().removeItemAtPath(path, error: nil)
and it still didn't help.
What I did:
Checked all my model files for the NSCoding protocol
Checked all my required init(coder aDecoder: NSCoder) and func encodeWithCoder(aCoder: NSCoder)
I am missing something, since I have done this in my last app and it worked fla`
import Foundation
private let ON_DISK_DATA_DICTIONARY = "savedDataPathsOnDisk"
private let _WBMAccessDataOnDiskMShared = WBMAccessDataOnDiskM()
private var dataDirectories:NSArray! = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
private var dataDirectoryURL:NSURL! = NSURL(fileURLWithPath: dataDirectories.objectAtIndex(0) as! String, isDirectory: true)
private var dataDirectoryPath:String! = dataDirectoryURL.path!
let FILE_FORMAT = ".archive"
class WBMAccessDataOnDiskM: NSObject
{
class var sharedData: WBMAccessDataOnDiskM
{
return _WBMAccessDataOnDiskMShared
}
private var dataAndPathDictionary = [String:String]()
func getDataAndPathDictionary() -> [String:String]
{
return self.dataAndPathDictionary
}
func addDataAndPathToDictionary(data:String ,path:String)
{
if !checkIfDataAllreadyExists(data)
{
let fullPath = createFullDataPath(path)
dataAndPathDictionary[data] = fullPath
NSUserDefaults.standardUserDefaults().setObject(dataAndPathDictionary, forKey: ON_DISK_DATA_DICTIONARY)
}
}
func checkIfDataIsAvailable(dataPathComponent:String) -> (Bool,String)
{
var paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! String
var dataPath = paths.stringByAppendingPathComponent(dataPathComponent)
var checkValidation = NSFileManager.defaultManager()
println(dataPathComponent)
if (checkValidation.fileExistsAtPath(dataPath))
{
return (true,dataPath)
}
else
{
return (false,"")
}
}
func checkForDataOnDisk() -> Bool
{
let dataDict = NSUserDefaults.standardUserDefaults().objectForKey(ON_DISK_DATA_DICTIONARY) as? [String:String]
if dataDict == nil
{
return false
}
else
{
dataAndPathDictionary = dataDict!
return true
}
}
private func checkIfDataAllreadyExists(data:String) -> Bool
{
let keys = self.dataAndPathDictionary.keys.array
if contains(keys, data)
{
return true
}
return false
}
private func createFullDataPath(path:String) -> String
{
var fullPathURL = dataDirectoryURL.URLByAppendingPathComponent(path + FILE_FORMAT)
return fullPathURL.path!
}
func saveDataArray(data:[AnyObject], path:String)
{
NSFileManager.defaultManager().removeItemAtPath(path, error: nil)
if NSKeyedArchiver.archiveRootObject(data, toFile: path)
{
// SAVING
println(" Saving data ARRAY ")
}
else
{
println(" NOT saving data ARRAY ")
}
}
func saveDataObject(dataObject:AnyObject, path:String)
{
if NSKeyedArchiver.archiveRootObject(dataObject, toFile: path)
{
println(" Saving data OBJECT ")
}
else
{
println(" NOT saving data OBJECT ")
}
}
// dataFromDisk = NSKeyedUnarchiver.unarchiveObjectWithFile(pathForNews) as? [AnyObject]
func loadDataArray(path:String) -> [AnyObject]?
{
var dataArrayFromDisk: [AnyObject]?
dataArrayFromDisk = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? [AnyObject]
return dataArrayFromDisk
}
func loadDataObject(path:String) -> AnyObject?
{
var dataObjectFromDisk: AnyObject?
dataObjectFromDisk = NSKeyedUnarchiver.unarchiveObjectWithFile(path)
return dataObjectFromDisk
}
func getNewsDataLanguagePath() -> String
{
var currentOSLanguage = LOCALIZATION.currentOsLanguage
currentOSLanguage = currentOSLanguage.substringToIndex(2)
if currentOSLanguage == "de"
{
return ON_DISK_CONTENT_DE
}
else if currentOSLanguage == "en"
{
return ON_DISK_CONTENT_ENG
}
return ON_DISK_CONTENT_ENG
}
`
I am using Xcode 6.4 and Swift 1.2.
Any help & code correction is welcome.

Because of the code you put here does't contain the call of saveDataArray or saveDataObject so I judge that you have maintain the path of a archived object manually.This is where thing went wrong. The method of NSKeyedArchiver named archiveRootObject can automatically maintain the archiver file path.
In the Apple's doucumet
Archives an object graph rooted at a given object by encoding it into a data object then atomically writes the resulting data object to a file at a given path, and returns a Boolean value that indicates whether the operation was successful.
And there is another question in SO may help you.

I followed apple instructions in this good example: Persist Data
But I had the same problem you describe with my app for AppleTV. At the end I change .Documents directory for CacheDirectory and it's working well.
static let DocumentsDirectorio = NSFileManager().URLsForDirectory(.CachesDirectory, inDomains: .UserDomainMask).first!

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")

why diskConfig.cachePathBlock should be reset to nil here?

I'm watching the source code of Kingfisher. And i have an question in the following code (Source code):
why reset diskConfig.cachePathBlock to nil at the end of init method?
I can't figure it out.
public convenience init(
name: String,
cacheDirectoryURL: URL?,
diskCachePathClosure: DiskCachePathClosure? = nil) throws
{
if name.isEmpty {
fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
}
let totalMemory = ProcessInfo.processInfo.physicalMemory
let costLimit = totalMemory / 4
let memoryStorage = MemoryStorage.Backend<Image>(config:
.init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit)))
var diskConfig = DiskStorage.Config(
name: name,
sizeLimit: 0,
directory: cacheDirectoryURL
)
if let closure = diskCachePathClosure {
diskConfig.cachePathBlock = closure
}
let diskStorage = try DiskStorage.Backend<Data>(config: diskConfig)
diskConfig.cachePathBlock = nil
self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)
}
Not setting the property to nil would keep a strong reference to the passed-in closure and potentially create a memory leak. Setting it to nil avoids this.
Assuming the nil assignment were not present, here's an example for a leak:
class CacheBuilder {
func createCache() -> ImageCache {
let cache = try! ImageCache(name: "Cache", cacheDirectoryURL: nil, diskCachePathClosure: self.path)
return cache
}
func path(_ url: URL, _ str: String) -> URL {
return url
}
deinit {
print("deinit") // is not called when Cache.init is done
}
}
class Cache {
let cache: ImageCache
init() {
let builder = CacheBuilder()
self.cache = builder.createCache()
}
}

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

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
}

Cannot convert value of type 'String?!' to expected argument type 'Notifications'

I am trying to check the id of a record before I put it into the array, using xcode swift
here is the code. But, i get the following error
Notifications.swift:50:46: Cannot convert value of type 'String?!' to expected argument type 'Notifications'
on this line
*if (readRecordCoreData(result["MessageID"])==false)*
Please can some one help to explain this error
import CoreData
struct Notifications{
var NotifyID = [NSManagedObject]()
let MessageDesc: String
let Messageid: String
init(MessageDesc: String, Messageid:String) {
self.MessageDesc = MessageDesc
self.Messageid = Messageid
// self.MessageDate = MessageDate
}
static func MessagesWithJSON(results: NSArray) -> [Notifications] {
// Create an empty array of Albums to append to from this list
var Notification = [Notifications]()
// Store the results in our table data array
if results.count>0 {
for result in results {
//get fields from json
let Messageid = result["MessageID"] as! String
let MessageDesc = result["MessageDesc"] as? String
let newMessages = Notifications(MessageDesc: MessageDesc!, Messageid:Messageid)
//check with id's from core data
if (readRecordCoreData(result["MessageID"])==false)
{
Notification.append(newMessages)
}
}
}
return Notification
}
//check id
func readRecordCoreData(Jsonid: String) -> Bool {
var idStaus = false
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
//2
let fetchRequest = NSFetchRequest(entityName: "ItemLog")
//3
do {
let resultsCD = try! managedContext.executeFetchRequest(fetchRequest)
if (resultsCD.count > 0) {
for i in 0 ..< resultsCD.count {
let match = resultsCD[i] as! NSManagedObject
let id = match.valueForKey("notificationID") as! String
if (Jsonid as String! == id)
{
idStaus = true
}
else{
idStaus = false
}
}
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
return idStaus
}
One of your methods is static and the other one is not :
func readRecordCoreData(Jsonid: String) -> Bool
static func MessagesWithJSON(results: NSArray) -> [Notifications]
Depending on what you want to accomplish you could declare both static, none, or replace
//check with id's from core data
if (readRecordCoreData(result["MessageID"])==false)
{
Notification.append(newMessages)
}
By
//check with id's from core data
if (Notifications.readRecordCoreData(Messageid)==false)
{
Notification.append(newMessages)
}
Not sure if the code will work past compilation however as there are many readability issues

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]