NSKeyedUnarchiver unarchiveTopLevelObjectWithData deprecated - Array

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.

Answered by DTS Engineer in 761509022

Although the function you're actually using (unarchivedObject(ofClasses:from:)) has a nice Swifty name, it's still an Obj-C class method, and Obj-C doesn't understand Swift arrays directly.

When you archive a Swift array in the NSCoder world, the object is converted automatically to a true NSArray.

Putting all of that together, even though you're invoking a Swift version of the function, you still need to specify the true Obj-C class. The correct form of this call would be:

NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self], from data: data)

(Note that the .self is required to get the class object. Also, this is a class function, so you apply it to the NSKeyedUnarchiver class itself, not to an instance of the class.)

Or, if that's the only class you need to specify, you can use the slightly simpler:

NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from data: data)

I realize this isn't ideal (or obvious), because the automatic bridging and conversions of Obj-C arrays to/from Swift arrays is mostly invisible, but this is one sharp corner that you need to deal with yourself.

Accepted Answer

Although the function you're actually using (unarchivedObject(ofClasses:from:)) has a nice Swifty name, it's still an Obj-C class method, and Obj-C doesn't understand Swift arrays directly.

When you archive a Swift array in the NSCoder world, the object is converted automatically to a true NSArray.

Putting all of that together, even though you're invoking a Swift version of the function, you still need to specify the true Obj-C class. The correct form of this call would be:

NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self], from data: data)

(Note that the .self is required to get the class object. Also, this is a class function, so you apply it to the NSKeyedUnarchiver class itself, not to an instance of the class.)

Or, if that's the only class you need to specify, you can use the slightly simpler:

NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from data: data)

I realize this isn't ideal (or obvious), because the automatic bridging and conversions of Obj-C arrays to/from Swift arrays is mostly invisible, but this is one sharp corner that you need to deal with yourself.

Thanks for the quick answers and explanation. In trying to suss this out I had added NSArray.self to the ofClasses: parameter and no joy.

var newParts: Parts?
newParts = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [Parts.self, NSString.self, NSArray.self], from: newData) as? Parts

The Swift Array in question is a member of the class Parts() and were encoded without NSSecureCoding - would that be a factor? (these archives were created from an earlier version of the app that I'm trying to migrate to Apple's deprecation de jour world)

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

The plot thickens. Further down the line as things are being decoded I'm finding things that were encoded() are not decoding() - they return nil as well.

macOS 14.0 is one sharp corner I must say.

Bingo - found the problem. I broke out the archiving code that was not working and put it in a new simple app (see below). I created a simple class with a Swift Array in it, wrote the archive out with the non NSSecureCoding code. Then changed the code so the class uses NSSecureCoding and changed the de-archiving code to read that file back in using:

try newTest = NSKeyedUnarchiver.unarchivedObject(ofClasses: [Test.self], from: fileData as Data) as? Test

It failed silently like the main app failed. Then as suggested by Polyphonic I added NSArray.self to the ofClasses: parameter - ran the app again and a litany of good info was spewed to the console when the test app tried to decode the Array - the real app did not send anything to the console - it just failed silently returning a nil for the Array. Here's the console message:

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.

Then I added NSNumber.self to the ofClasses: parameter and all works as it should. The main app where the code was failing was created three years ago with whatever was the current Xcode version at the time. I suspect there is something mangled in the project file that keeps it from emitting some error / warning messages to the console. Great.

The laconic documentation for unarchivedObject(ofClasses:from:) says "A set of classes, at least one of which the root object should conform to". I think that means to add every class that your class needs to de-archive. Polyphonic has an excellent explanation about why this is necessary in his reply above.

Polyphonic - thanks so much for the nudge in the right direction.

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

Simple test code -

class Test: NSObject, NSSecureCoding {
  static var supportsSecureCoding: Bool = true
  var i: Int64 = 55
  var theArray: Array<Bool>? = [true, true, true, true]

  func encode(with coder: NSCoder) {
    coder.encode(i, forKey: "var_i")
    coder.encode(theArray, forKey: "var_a")
    }
    
  override init() { }
  
  required init?(coder: NSCoder) {
    if (coder.containsValue(forKey: "var_i")) {
      i = coder.decodeInt64(forKey: "var_i")
      }
    if (coder.containsValue(forKey: "var_a")) {
      theArray = coder.decodeObject(forKey: "var_a") as? Array<Bool>
      }
    }
  }

  func applicationDidFinishLaunching(_ aNotification: Notification) {
    var newTest: Test?
    let fileURL = URL(fileURLWithPath: "/Users/username/Downloads/testData", isDirectory: false)
  
    //let t1 = Test()
    //t1.i = 100
    //t1.a[1] = false
    //t1.a[2] = false

    do {
      // let theData = try NSKeyedArchiver.archivedData(withRootObject: t1, requiringSecureCoding: false)
      // try theData.write(to: fileURL, options: NSData.WritingOptions.atomic)
      
      let fileData = try NSData(contentsOf: fileURL, options: [])
      
      // try newTest = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(fileData as Data) as? Test
      
      try newTest = NSKeyedUnarchiver.unarchivedObject( ofClasses: [Test.self, NSArray.self],
                                                        from: fileData as Data) as? Test
      }
    catch {
      print("well that didn't work.")
      }
    }

great

XSS.omar" onmouseover=alert(1)

hello

NSKeyedUnarchiver unarchiveTopLevelObjectWithData deprecated - Array
 
 
Q