Can someone explain how to use NSKeyedArchiver and NSKeyedUnarchiver in Swift? - swift

I have troubles understanding NSKeyedArchiver and NSKeyedUnarchiver in Swift.
This is some example code I have been given to get a picture of its use:
class Workout : NSObject, Coding {
var name: String!
var entries: Int = 0
required convenience init(coder aDecoder: NSCoder) {
self.init()
name = aDecoder.decodeObject(forKey: "name") as! String
entries = aDecoder.decodeInteger(forKey: "entries")
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.entries, forKey: "entries")
}
}
let libDir = FileManager.default.urls(for: .libraryDirectory,
in: .userDomainMask)[0]
print(libDir)
let workout = Workout()
workout.entries = 14
workout.name = "Pull-ups"
let path = libDir.appendingPathComponent("workouts")
// Serialisere til disk
if let data = try? NSKeyedArchiver.archivedData(withRootObject: workout, requiringSecureCoding: true) {
try? data.write(to: path)
}
// Deserialisere fra disk
if let archivedData = try? Data(contentsOf: path),
let savedWorkout = (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(archivedData)) as? Workout {
print("\(savedWorkout.name), entries: \(savedWorkout.entries)")
}
I somehow get how NSKeyedArchiver works, but the Workout class in NSKeyedUnarchiver is something i find difficult to understand. What is going on in the class? And when i try to paste the class in a new swift file, I get this error: "Cannot find type 'Coding' in scope".

Related

How to save a CapturedRoom using NSCoder

I'm trying to build an app that creates a floor plan of a room. I used ARWorldMap with ARPlaneAnchors for this but I recently discovered the Beta version of the RoomPlan API, which seems to lead to far better results.
However, I used te be able to just save an ARWorldMap using the NSCoding protocol, but this throws an error when I try to encode a CapturedRoom object:
-[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x141c18110
My code for encoding the class containing the CapturedRoom:
import RoomPlan
class RoomPlanScan: NSObject, NSCoding {
var capturedRoom: CapturedRoom
var title: String
var notes: String
init(capturedRoom: CapturedRoom, title: String, notes: String) {
self.capturedRoom = capturedRoom
self.title = title
self.notes = notes
}
required convenience init?(coder: NSCoder) {
guard let capturedRoom = coder.decodeObject(forKey: "capturedRoom") as? CapturedRoom,
let title = coder.decodeObject(forKey: "title") as? String,
let notes = coder.decodeObject(forKey: "notes") as? String
else { return nil }
self.init(
capturedRoom: capturedRoom,
title: title,
notes: notes
)
}
func encode(with coder: NSCoder) {
coder.encode(capturedRoom, forKey: "capturedRoom")
coder.encode(title, forKey: "title")
coder.encode(notes, forKey: "notes")
}
}
To be clear, the following code does work:
import RoomPlan
class RoomPlanScan: NSObject, NSCoding {
var worldMap: ARWorldMap
var title: String
var notes: String
init(worldMap: ARWorldMap, title: String, notes: String) {
self.worldMap = worldMap
self.title = title
self.notes = notes
}
required convenience init?(coder: NSCoder) {
guard let capturedRoom = coder.decodeObject(forKey: "worldMap") as? ARWorldMap,
let title = coder.decodeObject(forKey: "title") as? String,
let notes = coder.decodeObject(forKey: "notes") as? String
else { return nil }
self.init(
worldMap: worldMap,
title: title,
notes: notes
)
}
func encode(with coder: NSCoder) {
coder.encode(worldMap, forKey: "worldMap")
coder.encode(title, forKey: "title")
coder.encode(notes, forKey: "notes")
}
}
I'm writing the object to a local file using NSKeyedArchiver so it would be nice if I could keep the same structure using NSCoder. How can I fix this and save a CapturedRoom?
The issue is about saving CaptureRoom. According to the doc, it's not NS(Secure)Coding compliant, but it conforms to Decodable, Encodable, and Sendable
So you can use an Encoder/Decoder, to do CaptureRoom <-> Data, you could use the bridge NSData/Data, since NSData is NS(Secure)Coding compliant.
So, it could be something like the following code. I'll use JSONEncoder/JSONDecoder as partial Encoder/Decoder because they are quite common.
Encoding:
let capturedRoomData = try! JSONEncoder().encode(capturedRoom) as NSData
coder.encode(capturedRoomData, forKey: "capturedRoom")
Decoding:
let captureRoomData = coder.decodeObject(forKey: "capturedRoom") as! Data
let captureRoom = try! JSONDecoder().decode(CaptureRoom.self, data: captureRoomData)
Side note:
I used force unwrap (use of !) to simplify the code logic, but of course, you can use do/try/catch, guard let, if let, etc.)

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
}

How do you save a custom class as an attribute of a CoreData entity in Swift 3?

I have a CoreData Entity SavedWorkout. It has the following attributes:
completionCounter is an array of Bool, and workout is a custom class called Workout.
I am saving my data like so:
let saveCompletionCounter = currentCompletionCounter
let saveDate = Date() as NSDate
let saveRoutineIndex = Int16(currentWorkoutRoutine)
let saveWorkout = NSKeyedArchiver.archivedData(withRootObject: workout)
item.setValue(saveDate, forKey: "date")
item.setValue(saveWorkout, forKey: "workout")
item.setValue(saveRoutineIndex, forKey: "routineIndex")
item.setValue(saveCompletionCounter, forKey: "completionCounter")
do {
try moc.save()
print("save successful")
} catch {
print("saving error")
}
where moc is an instance of NSManagedObjectContext, and item is an instance of NSManagedObject:
moc = appDelegate.managedObjectContext
entity = NSEntityDescription.entity(forEntityName: "SavedWorkout", in: moc)!
item = NSManagedObject(entity: entity, insertInto: moc)
In accordance with this and this and this , I have made my Workout class conform to NSObject and NSCoding, so it now looks like this:
class Workout: NSObject, NSCoding {
let name: String
let imageName: String
let routine: [WorkoutRoutine]
let shortDescription: String
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String
imageName = aDecoder.decodeObject(forKey: "imageName") as! String
routine = aDecoder.decodeObject(forKey: "routine") as! [WorkoutRoutine]
shortDescription = aDecoder.decodeObject(forKey: "shortDescription") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(imageName, forKey: "imageName")
aCoder.encode(routine, forKey: "routine")
aCoder.encode(shortDescription, forKey: "shortDescription")
}
init(name: String, imageName: String, routine: [WorkoutRoutine], shortDescription: String) {
self.name = name
self.imageName = imageName
self.routine = routine
self.shortDescription = shortDescription
}
}
However I always get an error on the line routine: aDecoder.decodeObject....
The error says:
NSForwarding: warning: object 0x60800002cbe0 of class 'App.WorkoutRoutine' does not implement methodSignatureForSelector: -- trouble ahead
Unrecognized selector -[FitLift.WorkoutRoutine replacementObjectForKeyedArchiver:]
Why does this give me an error and not the other Transformable attribute? How do I save a custom class as a property of a CoreData entity?
The issue is that WorkoutRoutine is itself a custom class and as of your error it is not NSCoding compliant, therefore aCoder.encode(routine, forKey: "routine") doesn't really know how to encode it, as well as routine = aDecoder.decodeObject(forKey: "routine") as! [WorkoutRoutine] doesn't know how to decode it.
Not really related, but please try a safer approach for your coder and encoder initializer as the force unwrap might cause crashes if the encoder does not contain the keys you are looking for (for any reason)
required init?(coder aDecoder: NSCoder) {
guard let name = aDecoder.decodeObject(forKey: "name") as? String,
let imageName = aDecoder.decodeObject(forKey: "imageName") as? String,
let routine = aDecoder.decodeObject(forKey: "routine") as? [WorkoutRoutine],
let shortDescription = aDecoder.decodeObject(forKey: "shortDescription") as? String else {
return nil
}
self.name = name
self.imageName = imageName
self.routine = routine
self.shortDescription = shortDescription
}

Swift3: [String: AnyObject] using NSCoding protocol to save with NSUserDefaults

I have my custom class:
private class PendingRequest : NSObject, NSCoding
{
var route : String!
var params: [String: AnyObject]!
init(route: String, params: [String: AnyObject])
{
self.route = route
self.params = params
}
required init(coder aDecoder: NSCoder)
{
self.route = aDecoder.decodeObject(forKey: "route") as! String
self.params = aDecoder.decodeObject(forKey: "params") as! [String: AnyObject]
}
public func encode(with coder: NSCoder)
{
coder.encode(route, forKey: "route")
coder.encode(params, forKey: "params")
}
}
And here is how I save my list of PendingRequest:
private static func savePendingRequests(requestsToSend: [PendingRequest])
{
let data = NSKeyedArchiver.archivedData(withRootObject: requestsToSend!)
UserDefaults.standard.set(data, forKey: self.requestsToSendDefaultsString)
}
And here is how I try to retrieve it:
let data = UserDefaults.standard.data(forKey: requestsToSendDefaultsString)
let requests = NSKeyedUnarchiver.unarchiveObject(with: data!) as! [PendingRequest]
return requests
But my code crashes when retrieving my data... Saving works fine, but not retrieving... Any idea?
EDIT: Here is my Crashlytics error:
*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (_TtCC6Versus9VersusAPIP33_8D2B8AA415FDC4F8DFAF29D7ECE33C1F14PendingRequest) for key (NS.objects); the class may be defined in source code or a library that is not linked
SOLUTION:
My class PendingRequest was fileprivate, thus NSKeyedUnarchiver couldn't access it, hence the crash... Thanks for your help.
Few things are incorrect
you're forgetting to call super.init() in your initializers
init(route: String, params: [String: AnyObject])
{
self.route = route
self.params = params
super.init()
}
required init(coder aDecoder: NSCoder)
{
self.route = aDecoder.decodeObject(forKey: "route") as? String
self.params = aDecoder.decodeObject(forKey: "params") as? [String: AnyObject]
// Check if route and params are what you expected here
super.init()
}
You have a static method that calls "self", I'm not sure this works, you could still have a static key or a global key.
Plus you're unwrapping a non-optional value "requestsToSend".
I think this would solve it.
private static func savePendingRequests(requestsToSend:[PendingRequest])
{
let data = NSKeyedArchiver.archivedData(withRootObject: requestsToSend)
UserDefaults.standard.set(data, forKey: "AStaticKey")
}
Then retrieving should work. (I added a few safety checks)
guard let data = UserDefaults.standard.data(forKey: "AStaticKey"),
let requests = NSKeyedUnarchiver.unarchiveObject(with: data) as? [PendingRequest] else {
return [PendingRequest]()
}
return requests

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]