Round-trip encoding and decoding with NSKeyedArchiver and NSKeyedUnarchiver - swift

In the process of implementing init(coder:) for a custom NSView subclass, I came across some strange behavior with NSKeyedArchiver and NSKeyedUnarchiver that I still don't entirely understand. Consider this sample code:
let label = NSTextField(labelWithString: "Test")
// Encode
let data = try NSKeyedArchiver.archivedData(withRootObject: label, requiringSecureCoding: false)
// Decode
try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSTextField
This appears to encode and decode an NSTextField as expected. However, if I try to use decodeTopLevelObject() instead of unarchiveTopLevelObjectWithData(_:), the result is nil:
// Encode
let data = try NSKeyedArchiver.archivedData(withRootObject: label, requiringSecureCoding: false)
// Decode
let decoder = try NSKeyedUnarchiver(forReadingFrom: data)
decoder.decodeTopLevelObject() as? NSTextField // nil
Similarly, if I try to use encodedData instead of archivedData(withRootObject:requiringSecureCoding:), the result is nil:
// Encode
let coder = NSKeyedArchiver(requiringSecureCoding: false)
coder.encodeRootObject(label)
let data = coder.encodedData
// Decode
try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSTextField // nil
The result is even nil if I use encode(_:forKey:) and decodeObject(forKey:):
// Encode
let coder = NSKeyedArchiver(requiringSecureCoding: false)
coder.encode(label, forKey: "label")
let data = coder.encodedData
// Decode
let decoder = try NSKeyedUnarchiver(forReadingFrom: data)
decoder.decodeObject(forKey: "label") as? NSTextField // nil
I'm surprised that the first example above appears to work correctly but none of the others do (especially the last one). Could someone help me understand what's going on here?

If you read the documentation for init(forReadingFrom:) it states:
This initializer enables requiresSecureCoding by default....
This has probably been the main source of your confusion. Setting requiresSecureCoding back to false, then, will make the following work:
/* ENCODING */
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
archiver.encodeRootObject(label) // same as .encode(label)
archiver.encode(label, forKey: "SOME_CUSTOM_KEY")
archiver.finishEncoding() // as per documentation
let data = archiver.encodedData
/* DECODING */
let unarchiver = try! NSKeyedUnarchiver(forReadingFrom: data)
// DON'T FORGET THIS!!
unarchiver.requiresSecureCoding = false
let firstResult = unarchiver.decodeTopLevelObject() as! NSTextField . // same as .decodeObject()
let secondResult = unarchiver.decodeObject(forKey: "SOME_CUSTOM_KEY") as! NSTextField
unarchiver.finishDecoding() // as per documentation
When it comes to encoding and decoding correctly, just make sure you have matching keys. encodeRootObject(_:), which is implemented the same as encode(_:), internally uses the key of nil, so then just call decodeTopLevelObject(), or decodeObject().
On the other hand, NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:) uses the key NSKeyedArchiveRootObjectKey, so you could technically decode by performing:
let value = unarchiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as! NSTextField
...but you wouldn't want to do this, since it's an internal implementation that theoretically could change. Instead you'd just use NSKeyedArchiver.unarchiveTopLevelObjectWithData(_:), as you did in your working example.
Note: if you are using secure coding, there are other considerations to be made, but I think that's beyond the scope of this question.

Related

Cocoa: how to unarchive a NSMutableAttributedString with archived attributes?

I try to unarchive an archived NSMutableAttributedString in a macos-app, written in swift. I get back the text but without any attributes. The attributes are in the exported serialisation but it is ignored by NSMutableAttributedString(coder: unarchiver)!
Let's dive in: first I create a AttributedString and format one word with a red background:
let content = NSMutableAttributedString(string: "Dogs like to play with balls.")
content.addAttributes([NSAttributedString.Key.backgroundColor: NSColor.red], range: NSMakeRange(5, 4))
Then I serialise it with a NSKeyedArchiver:
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
archiver.outputFormat = .xml
content.encode(with: archiver)
archiver.finishEncoding()
I stored the result in a variable:
let serialisedData = archiver.encodedData
to write it back into a new NSMutableAttributedString:
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: serialisedData)
let restored = NSMutableAttributedString(coder: unarchiver)!
the restored AttributedString does not have any formations. :-(
Is there is a way to restore the attributes? I can see that the color information is in the serialisedData stream. I tried outputFormat with .xml and .binary with same results.
Here is the Playground output:
Got it!
let archiver = try NSKeyedArchiver.archivedData(withRootObject: content, requiringSecureCoding: false)
and
let restored = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSMutableAttributedString.self, from: archiver)
Doh!

how to store [UIImage] into Core Data and retrieve to [UIImage] from binary date

my code is like these:
//save to core data
func addPaper(){
let paper = Paper(context: self.context)
paper.id = UUID()
paper.subject = self.subject
paper.score = Float(self.score) ?? 0
paper.title = self.title
let imgs = try? NSKeyedArchiver.archivedData(withRootObject: self.images, requiringSecureCoding: true)
paper.images = imgs
try? self.context.save()
}
//retrieve to [UIImage]
let imgs = NSKeyedUnarchiver.unarchivedObject(ofClass: [UIImage], from: paper.images!)
there is an error tip:Static method 'unarchivedObject(ofClass:from:)' requires that '[UIImage]' conform to 'NSCoding'
I don't know what to do next, can anyone give me some help?
You should not store image in CoreData. Maybe store in base64 format, and you can encode/decode whenever you want.
Base64 To image:
func base64ToImage(data: String) -> Data{
let encodedImageData = data
let imageData = Data(base64Encoded: encodedImageData)
return imageData!
}
imageView.image = UIImage(data: dataDecoded)
Image to base64
func imageToBase64(image: UIImage) -> String {
return image.pngData()!
.base64EncodedString()
}
maybe you need this, any object can be saved to core data.
:https://stackoverflow.com/questions/56453857/how-to-save-existing-objects-to-core-data
I copied the following steps from others, hope it helps
1.making your custom types (Exercise) subclass of NSObject
2.setting the attribute's type in the core data model to Transformable
3.setting the CustomClass to [Exercise]

Retrieve NSDictionary Value with swift 4

I try to retrieve exif data from a picture.
I can load it in a Dictionary, but I am unable to use this Dictionary.
my Current code is :
import Cocoa
import ImageIO
let path = "/Volumes/Olivier/Original/Paysage/affoux/_OPI7684.NEF"
let UrlPath = URL(fileURLWithPath: path)
UrlPath.isFileURL
UrlPath.pathExtension
UrlPath.hasDirectoryPath
let imageSource = CGImageSourceCreateWithURL(UrlPath as CFURL, nil)
let imageProp = CGImageSourceCopyPropertiesAtIndex(imageSource!, 0, nil)
var key = "kCGImagePropertyWidth" as NSString
let h :NSDictionary = CFDictionaryGetValue(imageProp, kCGImagePropertyWidth)
The last line, doesn't work at all.
Any solution ?
Thank's
The problem is that your key name is wrong. You mean kCGImagePropertyPixelWidth. And it's not a string. It's a constant. So it should not be in quotes; just use the constant directly, and don't worry what its value is.
I would suggest also that you convert to a Swift dictionary earlier in the process. Here is actual working code that you can model yourself after:
let src = CGImageSourceCreateWithURL(url as CFURL, nil)!
let result = CGImageSourceCopyPropertiesAtIndex(src, 0, nil)!
let d = result as! [AnyHashable:Any]
let width = d[kCGImagePropertyPixelWidth] as! CGFloat
let height = d[kCGImagePropertyPixelHeight] as! CGFloat
Of course that code is pretty bad because every single line contains an exclamation mark (which means "crash me"), but in real life I don't crash, so I've allowed it to stand.

Using Guard in an Init?

Everything works swimmingly except for when I do a random string like "fds", how would I correctly and efficiently use a guard to protect from this sort of error?
init(weatherData: [String: AnyObject]) {
city = weatherData["name"] as! String
let weatherDict = weatherData["weather"]![0] as! [String: AnyObject]
description = weatherDict["description"] as! String
icon = weatherDict["icon"] as! String
let mainDict = weatherData["main"] as! [String: AnyObject]
currentTemp = mainDict["temp"] as! Double
humidity = mainDict["humidity"] as! Int
let windDict = weatherData["wind"] as! [String: AnyObject]
windSpeed = windDict["speed"] as! Double
}
how would I correctly and efficiently use a guard to protect from this sort of error?
Why would you want to? If the caller does not hand you a dictionary whose "name" key is present and is a string, you are dead in the water because you cannot initialize city. You want to crash.
If you would like to escape from this situation without actually crashing, then make this a failable initializer and fail (return nil) if the dictionary doesn't contain the needed data. This effectively pushes the danger of crashing onto the caller, because the result will be an Optional that might be nil, and the caller must check for that.
init?(weatherData: [String: AnyObject]) {
guard let city = weatherData["name"] as? String else {return nil}
self.city = city
// ... and so on ...
}
But what I would do is none of those things. I would rewrite the initializer as init(city:description:icon:currentTemp:humidity:windSpeed:) and force the caller to parse the dictionary into the needed data. That way, if the data is not there, we don't even try to initialize this class in the first place. My argument would be that it is the caller's job to parse the dictionary; this class should have no knowledge of the structure of some complex dictionary pulled off the Internet (or whatever the source is).

EXC Bad Instruction

In was wondering why I keep getting this error message, EXC Bad Instruction could someone help me out and tell me why.
Here is the code.
func updateStocks() {
let stockManager:StockManagerSingleton = StockManagerSingleton.sharedInstance
stockManager.updateListOfSymbols(stocks)
//Repeat this method after 15 secs. (For simplicity of the tutorial we are not cancelling it never)
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(15 * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(),
{
self.updateStocks()
}
)
}
//4
func stocksUpdated(notification: NSNotification) {
let values = (notification.userInfo as! Dictionary<String,NSArray>)
let stocksReceived:NSArray = values[kNotificationStocksUpdated]!
stocks.removeAll(keepCapacity: false)
for quote in stocksReceived {
let quoteDict:NSDictionary = quote as! NSDictionary
let changeInPercentString = quoteDict["ChangeInPercent"] as! String
let changeInPercentStringClean: NSString = (changeInPercentString as NSString).substringToIndex((changeInPercentString as NSString).length-1)
stocks.append(quoteDict["symbol"] as! String,changeInPercentStringClean.doubleValue)
}
tableView.reloadData()
NSLog("Symbols Values updated :)")
}
}
The line with the error in it is,
let changeInPercentString = quoteDict["ChangeInPercent"] as! String
The error states that Swift attempted to unwrap a nil value, as you stated on this line
let changeInPercentString = quoteDict["ChangeInPercent"] as! String
Swift attempts to force setting the value of quoteDict["ChangeInPercent"] to a String, because you use as!, instead, you should use as?, which will set the value to nil if the value cannot be found
let changeInPercentString = quoteDict["ChangeInPercent"] as? String
You could set this to a default value by using the ?? operator. For example, if you wanted the default value to be 0.0%, you could use
let changeInPercentString = (quoteDict["ChangeInPercent"] as? String) ?? "0.0%"
The inherent problem is most likely either that quoteDict["ChangeInPercent"] does not exist, or quoteDict["ChangeInPercent"] is not a String - it may be an NSString or simply a Double value.
If you find out that it is supposed to be an NSString, for example, you will need to change how you cast the value
let changeInPercentString: NSString = (quoteDict["ChangeInPercent"] as? NSString) ?? "0.0%"