Swift 3: Safe Way to Decode Values with Nscoder

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



Leave a reply



Submit