Search code examples
arraysswiftnscodingnscoder

NSKeyedUnarchiver unarchiveTopLevelObjectWithData deprecated - Array loads as nil even though it's in the archive


Since

NSKeyedUnarchiver.unarchiveTopLevelObjectWithData 

has been deprecated as of macOS 14.0 I am trying to load legacy NSCoder archives now using NSSecureCoding set in the class and using:

coder.unarchivedObject(ofClasses classes: [AnyClass], from data: Data) 

to load the archive. The archive loads and most of the class members can be extracted. However, Swift Arrays always come out nil even though

coder.containsValue(forKey:)  

returns true. The arrays were encoded with:

var pageUsesClearBackground: Array<Bool> = [true, true, true, true]
coder.encode(pageUsesClearBackground, forKey: "CB_KEY")

Using:

pageUsesClearBackground = coder.decodeObject(forKey: "CB_KEY") as? Array<Bool>

Brings back an array with 0 values, even though if I use the non NSSecureCode version, the Array is populated.

pageUsesClearBackground [Bool]  0 values    

I suspect this has something to do with Arrays in Swift are a struct and do not conform to NSCoding. Odd that it worked encoding and decoding them for years. Any idea how to retrieve class members that are Arrays from an archive made before NSSecureCoding?

Apparently, NSSecureCoding is so secure you can't even get the data back.


Bingo - HangarRash's answer below is correct. You have to add all the classes your class needs. I created a smaller test app and moved the offending archiving problem to it. When I ran the smaller app it gave the following warning in the console:

NSCoderTest[11222:509325] [general] *** -[NSKeyedUnarchiver validateAllowedClass:forKey:] allowed unarchiving safe plist type ''NSNumber' (0x2140200a8) [/System/Library/Frameworks/Foundation.framework]' for key 'NS.objects', even though it was not explicitly included in the client allowed classes set: '{(
    "'NSCoderTest.Test' (0x100eb0700) , "'NSArray' (0x213ff2440) [/System/Library/Frameworks/CoreFoundation.framework]"
)}'. This will be disallowed in the future.

The app I was working on was created several versions of Xcode ago and under Xcode 14.1 it was not emitting any warnings.

Hey Apple, maybe a little more work on the documentation and a little less work on emojis?


Solution

  • Here's some sample code you can run in a Swift Playground that allows you to unarchive your array:

    class Foo: NSObject, NSSecureCoding {
        static var supportsSecureCoding: Bool { true }
    
        override init() {
            super.init()
        }
    
        func encode(with coder: NSCoder) {
            // This is based on the array in your question
            var pageUsesClearBackground: Array<Bool> = [true, true, true, true]
            coder.encode(pageUsesClearBackground, forKey: "CB_KEY")
        }
    
        required init?(coder: NSCoder) {
            // This happily unarchives the [Bool]
            if let pageUsesClearBackground = coder.decodeObject(of: [NSArray.self, NSNumber.self], forKey: "CB_KEY") as? [Bool] {
                print(pageUsesClearBackground) // Shows expected output
            } else {
                print("oops")
            }
        }
    }
    
    let foo = Foo()
    let data = try NSKeyedArchiver.archivedData(withRootObject: foo, requiringSecureCoding: true)
    let bar = try NSKeyedUnarchiver.unarchivedObject(ofClass: Foo.self, from: data)
    

    NSSecureCoding and related classes are all based on Objective-C. You need to specify types that support NSCoding (NSSecureCoding). So when unarchiving you need to tell the unarchiver that you have an NSArray of NSNumber instances. That is what the Swift [Bool] is really converted into.