Swift 3: Safe way to decode values with NSCoder?
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
}
Encode/Decoding Date with NSCoder in Swift 3?
As Matt, says, you can't encode an optional. Rather than force-unwrapping it, though, I would suggest adding an if let
and only adding the optionals to the archive if they contain a value:
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: PropertyKey.nameKey)
aCoder.encode(className, forKey: PropertyKey.classNameKey)
aCoder.encode(assignmentDescription, forKey: PropertyKey.assignmentDescriptionKey)
aCoder.encode(materials, forKey: PropertyKey.materialsKey)
if let date = dueDate {
aCoder.encode(date, forKey: PropertyKey.dueDateKey)
}
if let image = assignmentImage {
aCoder.encode(image, forKey: PropertyKey.dueDateKey)
}
}
And then in your init(coder:) method, check to see if the keys exist before decoding:
required convenience init?(coder aDecoder: NSCoder) {
//Required fields.
name = aDecoder.decodeObject(forKey: PropertyKey.nameKey) as! String
className = aDecoder.decodeObject(forKey: PropertyKey.classNameKey) as! String
//Optional fields.
assignmentDescription = aDecoder.containsValue(forKey: PropertyKey.assignmentDescriptionKey) as? String
materials = aDecoder.decodeObject(forKey: PropertyKey.materialsKey) as? String
if aDecoder.containsValue(forKey: PropertyKey.dueDateKey) {
dueDate = aDecoder.decodeObject(forKey: PropertyKey.dueDateKey) as! Date
} else {
dueDate = nil
if aDecoder.containsValue(forKey: PropertyKey.assignmentImageKey) {
assignmentImage = aDecoder.decodeObject(forKey: PropertyKey.assignmentImageKey) as? UIImage
} else {
assignmentImage = nil
}
//Must call designated initializer.
self.init(name: name, className: className, assignmentDescription: assignmentDescription!, materials: materials!, dueDate: dueDate, assignmentImage: assignmentImage)
}
EDIT:
I just created a sample project called Swift3PhoneTest on Github (link) that demonstrates using NSSecureCoding
to save a custom data container object.
It has both a non-optional and an optional property, and properly manages archiving and unarchiving when the optional property is nil.
How do I decode a Double using NSCoder, NSObject and Swift 3 for iOS
required convenience init?(coder aDecoder: NSCoder) {
// for safety, make sure to let "amount" as optional (adding as Double?)
let amount = aDecoder.decodeDouble(forKey:PropertyKey.amountKey) as Double?
self.init(date: date, amount: amount, description: description, image: image)
}
Swift 3: How to catch error if NSCoder decodeX function decodes wrong value type?
Using decodeInteger
on a non-integer key would raise an exception. Sadly, it's an NSException
which Swift cannot handle directly (see references below).
You need to first write a wrapper to handle ObjC exceptions in ObjC and bridge it over to Swift (inspired by this answer):
/// -------------------------------------------
/// ObjC.h
/// -------------------------------------------
#import <Foundation/Foundation.h>
@interface ObjC : NSObject
+ (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error;
@end
/// -------------------------------------------
/// ObjC.m
/// -------------------------------------------
#import "ObjC.h"
@implementation ObjC
+ (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (NSException *exception) {
NSMutableDictionary * userInfo = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
[userInfo setValue:exception.reason forKey:NSLocalizedDescriptionKey];
[userInfo setValue:exception.name forKey:NSUnderlyingErrorKey];
*error = [[NSError alloc] initWithDomain:exception.name
code:0
userInfo:userInfo];
return NO;
}
}
@end
Now you can catch the exception in Swift:
do {
try ObjC.catchException {
let age = aDecoder.decodeInteger(forKey: "firstName")
}
} catch {
print(error.localizedDescription)
}
References: Using ObjectiveC with Swift: Adopting Cocoa Design Patterns
Although Swift error handling resembles exception handling in Objective-C, it is entirely separate functionality. If an Objective-C method throws an exception during runtime, Swift triggers a runtime error. There is no way to recover from Objective-C exceptions directly in Swift. Any exception handling behavior must be implemented in Objective-C code used by Swift.
Fail to decode Int with NSCoder in Swift
The problem is that seq2
is not an Int
, but rather a Int?
optional. It cannot be represented as an Objective-C integer.
You can use decodeObject
:
required init?(coder aDecoder: NSCoder){
self.seq = aDecoder.decodeObject(forKey: "seq") as? NSNumber
self.seq2 = aDecoder.decodeObject(forKey: "seq2") as? Int
self.id = aDecoder.decodeObject(forKey: "id") as? String
self.value = aDecoder.decodeObject(forKey: "value") as? String
super.init()
}
or change it so it is not optional:
class Model : NSObject, NSCoding {
var seq: NSNumber?
var seq2: Int
var id: String?
var value: String?
init(seq: NSNumber, seq2: Int, id: String, value: String) {
self.seq = seq
self.seq2 = seq2
self.id = id
self.value = value
super.init()
}
required init?(coder aDecoder: NSCoder) {
self.seq = aDecoder.decodeObject(forKey: "seq") as? NSNumber
self.seq2 = aDecoder.decodeInteger(forKey: "seq2")
self.id = aDecoder.decodeObject(forKey: "id") as? String
self.value = aDecoder.decodeObject(forKey: "value") as? String
super.init()
}
func encode(with aCoder: NSCoder) {
aCoder.encode(seq, forKey: "seq")
aCoder.encode(seq2, forKey: "seq2")
aCoder.encode(id, forKey: "id")
aCoder.encode(value, forKey: "value")
}
override var description: String { return "<Model; seq=\(seq); seq2=\(seq2); id=\(id); value=\(value)>" }
}
NSCoder crash on decodeBool forKey (Xcode 8, Swift 3)
the trick is remove ! form the primitive types.
If you put ! you are saying "make an implicit-unwrapped optional" so the encoder will archive as NSNumber instead of Bool (or Int, Double).
If you remove ! the encoder will archive as Bool and things works as expected (I spent an "incident" and this solution is provided by Apple)
How can I get an NSCoder to encode/decode a Swift array of structs?
Here is a possible solution that encodes the UInt64
array as
an array of bytes. It is inspired by the answers to How to serialize C array with NSCoding?.
class MyObject: NSObject, NSCoding {
var values: [UInt64] = []
init(values : [UInt64]) {
self.values = values
}
// MARK: - NSCoding
required init(coder decoder: NSCoder) {
super.init()
var count = 0
// decodeBytesForKey() returns an UnsafePointer<UInt8>, pointing to immutable data.
let ptr = decoder.decodeBytesForKey("values", returnedLength: &count)
// If we convert it to a buffer pointer of the appropriate type and count ...
let buf = UnsafeBufferPointer<UInt64>(start: UnsafePointer(ptr), count: count/sizeof(UInt64))
// ... then the Array creation becomes easy.
values = Array(buf)
}
func encodeWithCoder(coder: NSCoder) {
// This encodes both the number of bytes and the data itself.
coder.encodeBytes(UnsafePointer(values), length: values.count * sizeof(UInt64), forKey: "values")
}
}
Test:
let obj = MyObject(values: [1, 2, 3, UInt64.max])
let data = NSKeyedArchiver.archivedDataWithRootObject(obj)
let dec = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! MyObject
print(dec.values) // [1, 2, 3, 18446744073709551615]
Update for Swift 3 (Xcode 8):
class MyObject: NSObject, NSCoding {
var values: [UInt64] = []
init(values : [UInt64]) {
self.values = values
}
// MARK: - NSCoding
required init(coder decoder: NSCoder) {
super.init()
var count = 0
// decodeBytesForKey() returns an UnsafePointer<UInt8>?, pointing to immutable data.
if let ptr = decoder.decodeBytes(forKey: "values", returnedLength: &count) {
// If we convert it to a buffer pointer of the appropriate type and count ...
let numValues = count / MemoryLayout<UInt64>.stride
ptr.withMemoryRebound(to: UInt64.self, capacity: numValues) {
let buf = UnsafeBufferPointer<UInt64>(start: UnsafePointer($0), count: numValues)
// ... then the Array creation becomes easy.
values = Array(buf)
}
}
}
public func encode(with coder: NSCoder) {
// This encodes both the number of bytes and the data itself.
let numBytes = values.count * MemoryLayout<UInt64>.stride
values.withUnsafeBufferPointer {
$0.baseAddress!.withMemoryRebound(to: UInt8.self, capacity: numBytes) {
coder.encodeBytes($0, length: numBytes, forKey: "values")
}
}
}
}
let obj = MyObject(values: [1, 2, 3, UInt64.max])
let data = NSKeyedArchiver.archivedData(withRootObject: obj)
let dec = NSKeyedUnarchiver.unarchiveObject(with: data) as! MyObject
print(dec.values) // [1, 2, 3, 18446744073709551615]
How to encode/decode a dictionary with Codable values for storage in UserDefaults?
You are mixing up NSCoding
and Codable
. The former requires a subclass of NSObject
, the latter can encode the structs and classes directly with JSONEncoder
or ProperListEncoder
without any Keyedarchiver
which also belongs to NSCoding
.
Your struct can be reduced to
struct Company: Codable {
var name : String
var initials : String
var logoURL : URL?
var brandColor : String?
}
That's all, the CodingKeys and the other methods are synthesized. I would at least declare name
and initials
as non-optional.
To read and save the data is pretty straightforward. The corresponding CompanyDefaults
struct is
struct CompanyDefaults {
static private let companiesKey = "companiesKey"
static var companies: [String:Company] = {
guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
return try? JSONDecoder.decode([String:Company].self, from: data) ?? [:]
}() {
didSet {
guard let data = try? JSONEncoder().encode(companies) else { return }
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}
Related Topics
Zoom to Fit Current Location and Annotation on Map
Why Can't I Use .Reduce() in a One-Liner Swift Closure with a Variadic, Anonymous Argument
Swift 3: Safe Way to Decode Values with Nscoder
Avplayer Seektotime Not Working Properly
Uibezierpath Appending Overlapping Isn't Filled
Swiftui View and Uihostingcontroller in Uiscrollview Breaks Scrolling
Does Swift Optimise Chained Creation and Copy of Structs
Swift: +[Catransaction Synchronize] Called Within Transaction While Decoding HTML Entities
How to Implement a Billboard Effect (Lookat Camera) in Realitykit
Error "No Such Module" When Installed Framework with Pod in Swift 3
Why Use an Extra Let Statement Here
Using @Discardableresult for Closures in Swift
Xcode Playgrounds Shared Directory Not Working
Swiftui: How to Switch to a New Navigation Stack with Navigationviews
Why Are Uiscreen.Bounds Incorrect in iOS11
Swift Generics Protocols: Can Only Be Used as a Generic Constraint Problem