Trouble decoding array objects via NSKeyedArchiver / NSSecureCoding

Hiya 👋! While loading some data, I'm having issues decoding arrays of basic types, specifically Int and Boolthey return nil. My app has existing data (live on the App Store for years) that is saved and loaded using NSKeyedArchiver. I'm updating to support a newer iOS version, which requires me now to adhere to NSSecureCoding. As I understand, NSSecureCoding needs strict type definitions, and it will return nil if things are ambiguous (for security reasons).

Essentially as I load data, it all works when I use strict types, like Int and Bool, because the methods themselves are strict: decodeInteger(forKey:) and decodeBool(forKey:). However, if I want to decode something more complex, like NSDate or basic type arrays (in my case [Int], [[[Int]]], [Bool] and [[Bool]]), I assume I have to decode an object: decodeObject(of:forKey:). While I was able to make NSDate work by forcing the type (decodeObject(of: NSDate.self, forKey: "modifyDate")! as Date), getting arrays to decode is proving difficult. They always return nil.

I've now tried forcing the type to different arrays, including NSArray, and listing array types. I also tried decodeArrayOfObjects(ofClass:forKey:), but no luck.

Example:

Here are some data items I might have:

var id: Int?
var isModified: Bool?
var modifyDate: Date?

var myIntegers: [Int]?
var myEmbedIntegers: [[[Int]]]?
var myBooleans: [Bool]?
var myEmbedBooleans: [[Bool]]?

Here's how I encode them:

encode(id!, forKey: "id")
encode(isModified!, forKey: "isModified")
encode(modifyDate!, forKey: "modifyDate")

encode(myIntegers!, forKey: "myIntegers")
encode(myEmbedIntegers!, forKey: "myEmbedIntegers")
encode(myBooleans!, forKey: "myBooleans")
encode(myEmbedBooleans!, forKey: "myEmbedBooleans")

This is how Int, Bool and Date are apparently successfully decoded (where Date is forced to search for NSDate type):

decodeInteger(forKey: "id")
decodeBool(forKey: "isModified")
decodeObject(of: NSDate.self, forKey: "modifyDate")! as Date

Here are attempts with Int (same with Bool) arrays, which are erroneously decoded (these all either get compiler errors or return nil):

decodeObject(forKey: "myIntegers") as! [Int] // This used to work before NSSecureCoding, but now returns nil.

decodeObject(of: [NSArray.self], forKey: "myIntegers") as! [Int] // Returns nil.

decodeObject(of: [Int.self], forKey: "myIntegers") // Compiler error about value type conversion.

decodeArrayOfObjects(ofClass: Int.self, forKey: "myIntegers") // Compiler error complaining that Int doesn't conform to NSSecureCoding.

decodeArrayOfObjects(ofClass: NSArray.self, forKey: "myIntegers") as! [Int] // Returns nil.

The funny thing is I don't even remotely need security. My data is for song compositions in an entertainment app, so it's strictly loaded and saved to device by my own code without networking, hashing or anything else being involved. Nevertheless I'm now stuck on this 😥.

How do I decode arrays (without returning nil)?

Answered by DTS Engineer in 817224022

Keyed archiving is very Objective-C centric. So, when you encode a Swift Array, it’s actually stored as an NSArray. NSArray can only hold objects. The Swift Bool and Int types are not objects. When you put them in a keyed archive, they get stored as NSNumber objects.

Normally this isn’t a big deal because the Swift compiler goes out of its way to help you convert the types. However, when working with a secure keyed archive you have to list the element type, and that has to be an Objective-C object type. So, to decode an array of Bool values, you have to tell the keyed archiving to decode an array of NSNumber objects.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Could you post the complete code of the class ?

Certainly 👌! The full class is below. Note that this is a mock example, but it does reflect and represent my real class pretty identically.

import Foundation

class MyData: NSObject, NSSecureCoding {
    
    public class var supportsSecureCoding: Bool { true }
    
    var id: Int?
    var isModified: Bool?
    var modifyDate: Date?

    var myIntegers: [Int]?
    var myEmbedIntegers: [[[Int]]]?
    var myBooleans: [Bool]?
    var myEmbedBooleans: [[Bool]]?
    
    init(_id: Int, _isModified: Bool, _modifyDate: Date, _myIntegers: [Int], _myEmbedIntegers: [[[Int]]], _myBooleans: [Bool], _myEmbedBooleans: [[Bool]]) {
        id = _id
        isModified = _isModified
        modifyDate = _modifyDate
        
        myIntegers = _myIntegers
        myEmbedIntegers = _myEmbedIntegers
        myBooleans = _myBooleans
        myEmbedBooleans = _myEmbedBooleans
    }
    
    required convenience init(coder decoder: NSCoder) {
        let id = decoder.decodeInteger(forKey: "id")
        let isModified = decoder.decodeBool(forKey: "isModified")
        let modifyDate = decoder.decodeObject(of: NSDate.self, forKey: "modifyDate")! as Date
        
        // Here's where the errors happen: The four lines underneath return nil, which in this case cause crashes due to force unwrapping.
        
        let myIntegers = decoder.decodeObject(forKey: "myIntegers") as! [Int]
        let myEmbedIntegers = decoder.decodeObject(forKey: "myEmbedIntegers") as! [[[Int]]]
        let myBooleans = decoder.decodeObject(forKey: "myBooleans") as! [Bool]
        let myEmbedBooleans = decoder.decodeObject(forKey: "myEmbedBooleans") as! [[Bool]]
        
        self.init(_id: id, _isModified: isModified, _modifyDate: modifyDate, _myIntegers: myIntegers, _myEmbedIntegers: myEmbedIntegers, _myBooleans: myBooleans, _myEmbedBooleans: myEmbedBooleans)
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(id!, forKey: "id")
        coder.encode(isModified!, forKey: "isModified")
        coder.encode(modifyDate!, forKey: "modifyDate")
        
        coder.encode(myIntegers!, forKey: "myIntegers")
        coder.encode(myEmbedIntegers!, forKey: "myEmbedIntegers")
        coder.encode(myBooleans!, forKey: "myBooleans")
        coder.encode(myEmbedBooleans!, forKey: "myEmbedBooleans")
    }
}

And they are called using these interfaces:

public func loadMyData() -> [MyData]? {
    do {
        let data = try Data(contentsOf: myDataPath())
        
        let myData = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: MyData.self, from: data)
        
        return myData
    } catch {}
    
    return nil
}

public func saveMyData(_ mixData: [MyData]) {
    do {
        let data = try NSKeyedArchiver.archivedData(withRootObject: myData, requiringSecureCoding: false)
        try data.write(to: myDataPath())
    } catch {}
}

private func myDataPath() -> URL {
    return (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! as URL).appendingPathComponent("MyData.plist")
}
Accepted Answer

Keyed archiving is very Objective-C centric. So, when you encode a Swift Array, it’s actually stored as an NSArray. NSArray can only hold objects. The Swift Bool and Int types are not objects. When you put them in a keyed archive, they get stored as NSNumber objects.

Normally this isn’t a big deal because the Swift compiler goes out of its way to help you convert the types. However, when working with a secure keyed archive you have to list the element type, and that has to be an Objective-C object type. So, to decode an array of Bool values, you have to tell the keyed archiving to decode an array of NSNumber objects.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks 🙏! I would've never guessed the issue was due to Objective-C types. With some trial and error I was able to get everything working, using decodeArrayOfObjects(ofClass:forKey:) for the simple arrays and decodeObject(of:forKey:) for the nested arrays. It was really important to list out NSNumber and NSArray, where appropriate, to avoid warnings.

Essentially this is what I ended up with:

let myIntegers = decodeArrayOfObjects(ofClass: NSNumber.self, forKey: "myIntegers") as! [Int]

let myEmbedIntegers = decoder.decodeObject(of: [NSArray.self, NSNumber.self], forKey: "myEmbedIntegers") as! [[[Int]]]

let myBooleans = decoder.decodeArrayOfObjects(ofClass: NSNumber.self, forKey: "myBooleans") as! [Bool]

let myEmbedBooleans = decoder.decodeObject(of: [NSArray.self, NSNumber.self], forKey: "myEmbedBooleans") as! [[Bool]]

And here's the whole amended class:

import Foundation

class MyData: NSObject, NSSecureCoding {
    
    public class var supportsSecureCoding: Bool { true }
    
    var id: Int?
    var isModified: Bool?
    var modifyDate: Date?

    var myIntegers: [Int]?
    var myEmbedIntegers: [[[Int]]]?
    var myBooleans: [Bool]?
    var myEmbedBooleans: [[Bool]]?
    
    init(_id: Int, _isModified: Bool, _modifyDate: Date, _myIntegers: [Int], _myEmbedIntegers: [[[Int]]], _myBooleans: [Bool], _myEmbedBooleans: [[Bool]]) {
        id = _id
        isModified = _isModified
        modifyDate = _modifyDate
        
        myIntegers = _myIntegers
        myEmbedIntegers = _myEmbedIntegers
        myBooleans = _myBooleans
        myEmbedBooleans = _myEmbedBooleans
    }
    
    required convenience init(coder decoder: NSCoder) {
        let id = decoder.decodeInteger(forKey: "id")
        let isModified = decoder.decodeBool(forKey: "isModified")
        let modifyDate = decoder.decodeObject(of: NSDate.self, forKey: "modifyDate")! as Date
        
        let myIntegers = decodeArrayOfObjects(ofClass: NSNumber.self, forKey: "myIntegers") as! [Int]
        let myEmbedIntegers = decoder.decodeObject(of: [NSArray.self, NSNumber.self], forKey: "myEmbedIntegers") as! [[[Int]]]
        let myBooleans = decoder.decodeArrayOfObjects(ofClass: NSNumber.self, forKey: "myBooleans") as! [Bool]
        let myEmbedBooleans = decoder.decodeObject(of: [NSArray.self, NSNumber.self], forKey: "myEmbedBooleans") as! [[Bool]]
        
        self.init(_id: id, _isModified: isModified, _modifyDate: modifyDate, _myIntegers: myIntegers, _myEmbedIntegers: myEmbedIntegers, _myBooleans: myBooleans, _myEmbedBooleans: myEmbedBooleans)
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(id!, forKey: "id")
        coder.encode(isModified!, forKey: "isModified")
        coder.encode(modifyDate!, forKey: "modifyDate")
        
        coder.encode(myIntegers!, forKey: "myIntegers")
        coder.encode(myEmbedIntegers!, forKey: "myEmbedIntegers")
        coder.encode(myBooleans!, forKey: "myBooleans")
        coder.encode(myEmbedBooleans!, forKey: "myEmbedBooleans")
    }
}

Thanks again for the expert help! I can finally move on to more enjoyable challenges 🤣.

Trouble decoding array objects via NSKeyedArchiver / NSSecureCoding
 
 
Q