Swift 3: Safe way to decode values with NSCoder? - swift

Before Swift 3, you decode boolean values with NSCoder like this:
if let value = aDecoder.decodeObjectForKey(TestKey) as? Bool {
test = value
}
The suggested approach in Swift 3 is to use this instead:
aDecoder.decodeBool(forKey: TestKey)
But the class reference for decodeBool doesn't explain how to handle the situation if the value you're decoding isn't actually a boolean. You can't embed decodeBool in a let statement because the return value isn't an optional.
How do you safely decode values in Swift 3?

Took me a long time to figure out but you can still decode values like this.
The problem I had with swift3 is the renaming of the encoding methods:
// swift2:
coder.encodeObject(Any?, forKey:String)
coder.encodeBool(Bool, forKey:String)
// swift3:
coder.encode(Any?, forKey: String)
coder.encode(Bool, forKey: String)
So when you encode a boolean with coder.encode(boolenValue, forKey: "myBool") you have to decode it with decodeBool but when you encode it like this:
let booleanValue = true
coder.encode(booleanValue as Any, forKey: "myBool")
you can still decode it like this:
if let value = coder.decodeObject(forKey: "myBool") as? Bool {
test = value
}

This is safe (for shorter code using nil-coalescing op.) when wanted to use suggested decodeBool.
let value = aDecoder.decodeObject(forKey: TestKey) as? Bool ?? aDecoder.decodeBool(forKey: TestKey)
Using decodeBool is possible in situations when sure that it's Bool, IMO.

Related

Top-level Bool encoded as number property list fragment. PropertyListEncoder

I have this generic function to save in NSUserDefaults, in generally works but now I want to save a boolean value and I get an error. I could not find anything and I do not understand why it is not working.
extension UserDefaults {
func saveUserDefaults<T: Codable>(withKey key: String, myType: T) throws{
do {
let data = try PropertyListEncoder().encode(myType)
UserDefaults.standard.set(data, forKey: key)
print("Saved for Key:", key)
} catch let error {
print("Save Failed")
throw error
}
}
I am calling it like this:
try! UserDefaults().saveUserDefaults(withKey: "String", myType: false)
This is the error I get. I know there is an other way to save boolean values, but I am wondering why it is not working like this?
Thread 1: Fatal error: 'try!' expression unexpectedly raised an error:
Swift.EncodingError.invalidValue(false,
Swift.EncodingError.Context(codingPath: [], debugDescription:
"Top-level Bool encoded as number property list fragment.",
underlyingError: nil))
Thanks!
A PropertyListEncoder encodes to a “property list,” and that is always an
array or a dictionary, compare PropertyListSerialization.
Therefore
let data = try PropertyListEncoder().encode(myType)
fails if myType is a Bool (or anything which is not an array
or a dictionary).
The possible objects in a property list are also restricted, they can only be instances of
NSData, NSString, NSArray, NSDictionary, NSDate, or NSNumber – or of Swift types
which are bridged to one of those Foundation types.
As #Martin said a PropertyListEncoder supports only property lists on top level, but not a single fragment of property list like NSNumber.
A very simple (though not very elegant) workaround is to wrap any object into array:
let data = try PropertyListEncoder().encode([myType])
UserDefaults.standard.set(data, forKey: key)
And decode it as:
let arr = try PropertyListDecoder().decode([T].self, from: data)
return arr.first
see https://www.marisibrothers.com/2018/07/workaround-for-serializing-codable-fragments.html
You do not need to encode a Boolean value to save into UserDefaults. You can directly save the boolean into the UserDefaults by calling
let myValue: Bool = false
UserDefaults.standard.set(myValue, forKey: "key")
See Apple docs: UserDefaults.set(_:forKey:).
In fact, Float, Double, Integer, Bool and URL types do not need to be encoded and can directly be saved to UserDefaults.
I see that you have a function that takes a Codable type, encodes it and then saves it to UserDefaults. As Martin R pointed out, you will have to modify that function and check whether passed object can be directly saved to UserDefaults without a need for encoding. It is not necessarily pretty but something like this could work:
switch objectToSave {
case let aFloatType as Float:
UserDefaults.standard.set(aFloatType, forKey: "key")
case let aDoubleType as Double:
UserDefaults.standard.set(aDoubleType, forKey: "key")
case let anIntegerType as Int:
UserDefaults.standard.set(anIntegerType, forKey: "key")
case let aBoolType as Bool:
UserDefaults.standard.set(aBoolType, forKey: "key")
case let aURLType as URL:
UserDefaults.standard.set(aURLType, forKey: "key")
default:
//encode the object as a PropertyList and then save it to UserDefaults
do {
let data = try PropertyListEncoder().encode(objectToSave)
UserDefaults.standard.set(data, forKey: key)
} catch let error {
//the object you passed cannot be encoded as a PropertyList
//possibly because the object cannot be represented as
//NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary - or equivalent Swift types
//also contents of NSArray and NSDictionary have to be one of the types above
print("Save Failed: \(error)")
//perform additional error handling
}
}

Why is this still optional?

I make a query that returns a NSNumber. I then attempt to cast the NSNumber to String. For some reason, it always prints/ compares as an optional...but when I check the variables type it says string...Why is it optional? I need it to be a string!
let whoseTurn = selectedGame?["currentSender"]
let whoseTurnAsString: String = String(describing: whoseTurn)
if let whoseTurn = selectedGame?["currentSender"] as? NSNumber {
let whoseTurnAsString = "\(whoseTurn)"
print(whoseTurnAsString)
}
This is the right way to do optional chaining and make sure you are not forcing an optional
whoseTurn is an optional wrapping your NSNumber. You aren't "casting" it here to a string, you are making a string that "describes" it, and in this case that description includes the fact that whoseTurn is an optional... If you don't want that you'll need to unwrap it,
let whoseTurnAsString: String = String(describing: whoseTurn!)
(note the ! at the end)
This line of code let whoseTurn = selectedGame?["currentSender"] will return an optional.
This line of code let whoseTurnAsString: String = String(describing: whoseTurn) will return a String describing that optional value, which will be a string like this: Optional(5) or Optional(6). It describes the value saying that it is an optional.
So you need to unwrap that optional in order to get the wrapped value, you can force unwrap selectedGame like this:
let whoseTurn = selectedGame!["currentSender"] and then use the normal String initializer like this: String(whoseTurn).
Or, preferably, safely unwrap it like this:
if let whoseTurn = selectedGame?["currentSender"] {
let whoseTurnAsString = String(whoseTurn)
}
String can be optional also '?' or '!' indicates optional, check documentation on optionals.

How to access CFDictionary in Swift 3?

I need to read and write some data from CFDictionary instances (to read and update EXIF data in photos). For the life of me, I cannot figure out how to do this in Swift 3. The signature for the call I want is:
func CFDictionaryGetValue(CFDictionary!, UnsafeRawPointer!)
How the heck do I convert my key (a string) to an UnsafeRawPointer so I can pass it to this call?
If you don't have to deal with other Core Foundation functions expecting an CFDictionary, you can simplify it by converting to Swift native Dictionary:
if let dict = cfDict as? [String: AnyObject] {
print(dict["key"])
}
Be careful converting a CFDictionary to a Swift native dictionary. The bridging is actually quite expensive as I just found out in my own code (yay for profiling!), so if it's being called quite a lot (as it was for me) this can become a big issue.
Remember that CFDictionary is toll-free bridged with NSDictionary. So, the fastest thing you can do looks more like this:
let cfDictionary: CFDictionary = <code that returns CFDictionary>
if let someValue = (cfDictionary as NSDictionary)["some key"] as? TargetType {
// do stuff with someValue
}
What about something like:
var key = "myKey"
let value = withUnsafePointer(to: &key){ upKey in
return CFDictionaryGetValue(myCFDictionary, upKey)
}
You can write something like this:
let key = "some key" as NSString
if let rawResult = CFDictionaryGetValue(cfDictionary, Unmanaged.passUnretained(key).toOpaque()) {
let result = Unmanaged<AnyObject>.fromOpaque(rawResult).takeUnretainedValue()
print(result)
}
But I guess you would not like to write such thing at any time you retrieve some data from that CFDictionary. You better convert it to Swift Dictionary as suggested in Code Different's answer.

swift using guard and typechecking in one line

I like using guard and came across the situation where I want to use where for a typecheck as well:
guard let status = dictionary.objectForKey("status") as! String! where status is String else { ...}
xCode complains correctly that it's always true.
My goal is to have an unwrapped String after the guard in one line.
How can I do this?
Probably you want this?
guard let status = dictionary["status"] as? String else {
// status does not exist or is not a String
}
// status is a non-optional String
When you use as! String! you tell Swift that you know that the object inside your dictionary must be a String. If at runtime the object is not a String, you let Swift throw a cast exception. That is why the where part of your check is not going to fail: either the status is going to be a String, or you would hit an exception prior to the where clause.
You can do an optional cast instead by using as? operator instead of as!. Coupled with guard let, this approach produces
guard let status = dictionary.objectForKey("status") as? String else { ... }
... // If you reached this point, status is of type String, not String?

how should I use "?" in Swift

for example
if let name = jsonDict["name"] as AnyObject? as? String {
println("name is \(name)")
} else {
println("property was nil")
}
I have the follow question:
jsonDict["name"] as AnyObject? as? String is the same as jsonDict["name"] as? AnyObject as? String yes or no?
jsonDict["name"] as AnyObject? as? String is the same as jsonDict["name"] as AnyObject? as String? yes or no?
I don't know the difference between as? String and as String?
jsonDict["name"] as AnyObject? as? String is the same as jsonDict["name"] as? AnyObject as? String yes or no? - No, the latter makes no sense, you are trying to making a double cast from AnyObject to String. Furthermore, jsonDict["name"] would be enough for the compiler to recognize what type the return is, you shouldn't need any casting to a string.
jsonDict["name"] as AnyObject? as? String is the same as jsonDict["name"] as AnyObject? as String? yes or no?. Same as the first case, making a double cast makes little sense. Furthermore, the difference between as? and as is that as? will only execute in the case that the object can be successfully converted to the desired type, if this is not the case, the object will not be casted, therefore avoiding crashes.
there is huge difference between as and as?.
as
the as downcasts the object and force-unwrap the result in every time, and it does not really care about the real type of the object or any fail-safe procedure. the responsibility is yours, therefore that can cause the following code crashes in runtime:
let jsonDict: Dictionary<String, AnyObject> = ["name": 32]
if let downcasted: String = jsonDict["name"] as String? { ... } else { ... }
the reason: it forcibly downcasts the Int32 value to an optional String (=String?), and that cause a simple crash – if force-unwrapping fails, it will cause crash in every occasion in runtime.
you can use this solution only when you are 100% certain the downcast won't fail.
therefore, if your dictionary looks like this:
let jsonDict: Dictionary<String, String?> = ["name": nil]
if let downcasted: String = jsonDict["name"] as String? { ... } else { ... }
the else-branch will be executed, because the String? is nil, when you replace the nil with any string (e.g. "My Name") in your dictionary, the if-branch will be executed in runtime as expected – if the value is any other type than optional String, a crash will happen as I highlighted above in every case when the value is not nil.
as?
the as? optionally downcast the object, and if the downcasting procedure fails then it returns nil. that solution helps you to check the success of the donwcasting in runtime without suffering any crash:
let jsonDict: Dictionary<String, AnyObject> = ["name": 32]
if let downcasted: String = jsonDict["name"] as? String { ... } else { ... }
in that situation the else-branch will executed and your application can carry on, because the actual Int32 value fails to be downcasted to String – if your value is a String (e.g. "My Name") then the if-branch will be executed in runtime.
you have to use this solution when you are not certain about which the actual object's type is in runtime and there is a chance the downcasting procedure could fail.
NOTE: I would recommend to read the official docs about type-casting and optional-chaining for better understanding the entire procedure.