Unarchiving an object with custom classes

I have a custom class named CodeReadModel, which contains another custom class named CodeDataModel. The former contains the latter as an array like the following.

class CodeReadModel: NSObject, NSSecureCoding {
    class var supportsSecureCoding: Bool { true }
    
    let identifier: String
    let codeDataModels: [CodeDataModel]
    init(identifier: String, codeDataModels: [CodeDataModel]) {
        self.identifier = identifier
        self.codeDataModels = codeDataModels
    }
    required init(coder decoder: NSCoder) {
        self.identifier = decoder.decodeObject(forKey: "identifier") as! String
        self.codeDataModels = decoder.decodeObject(forKey: "codeDataModels") as! [CodeDataModel]
    }
    func encode(with coder: NSCoder) {
        coder.encode(identifier, forKey: "identifier")
        coder.encode(codeDataModels, forKey: "codeDataModels")
    }
}

And I want to unarchive an object with the following.

func importCodeReaderSnippetNext(fileURL: URL) {
	do {
		NSKeyedUnarchiver.setClass(CodeReadModel.self, forClassName: "CodeReadModel")
		NSKeyedUnarchiver.setClass(CodeDataModel.self, forClassName: "CodeDataModel")
		let data = try! Data(contentsOf: fileURL)
		if let codeReadModel = try NSKeyedUnarchiver.unarchivedObject(ofClass: CodeReadModel.self, from: data) {
                
		}
	} catch {
		print("Error: \(error.localizedDescription)")
    }
}

And I will get an error because codeReadModel contains another custom class, which cannot be decoded. How can I resolve this problem? Muchas thankos.

Answered by DTS Engineer in 795727022

Over the years I’ve posted a number of secure coding examples, including here, here, and here. Please read those through and write back if you get stuck.

Share and Enjoy

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

Over the years I’ve posted a number of secure coding examples, including here, here, and here. Please read those through and write back if you get stuck.

Share and Enjoy

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

Thank you for your kind reply, Eskimo. The first topic that you have mentioned is mine. And it deals with a [String : Any] object. This topic doesn't. codeReadModel is not a [String : Any] object. I suppose that the second one deals with an array of objects. I don't know about the third one, though.

Silly me... I would have been more productive if I have taken a closer look at the topic that I have accepted as an answer here. Nonetheless, I still don't get it. I have done many variations like the following.

if let dict = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSArray.self, NSString.self, CodeReadModel.self, CodeDataModel.self, NSString.self], from: data) {
    print("**** \(dict)")
} else {
    print("What!?")
}

They all go to the second print.

Consider two classes like this:

class CodeDataModel: NSObject, NSSecureCoding {
    let data: Data
    
    init(data: Data) {
        self.data = data
        super.init()
    }
    
    …
}

class CodeReadModel: NSObject, NSSecureCoding {
    let identifier: String
    let codeDataModels: [CodeDataModel]

    init(identifier: String, codeDataModels: [CodeDataModel]) {
        self.identifier = identifier
        self.codeDataModels = codeDataModels
    }

    …
}

Here’s how you add secure coding support to the first:

class CodeDataModel: NSObject, NSSecureCoding {

    …
    
    class var supportsSecureCoding: Bool { true }
    
    required init?(coder: NSCoder) {
        guard let data = coder.decodeObject(of: NSData.self, forKey: "data") else {
            return nil
        }
        self.data = data as Data
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(self.data, forKey: "data")
    }
}

Note the following:

  • The initialiser is failable. In your code you’re using a non-failable initialiser. That compiles, due to Swift’s subtyping rules, but it’s not useful because you need a way to return an error.

  • I’m using decodeObject(of:forKey:), where I pass in the type. This is a critical aspect of secure coding.

  • I’m using NSData, because Data isn’t an object. After I check the result for nil I cast it to Data before assigning it to the property.

Now let’s add secure coding support to the second:

class CodeReadModel: NSObject, NSSecureCoding {
    …

    class var supportsSecureCoding: Bool { true }
    
    required init?(coder decoder: NSCoder) {
        guard
            let identifier = decoder.decodeObject(of: NSString.self, forKey: "identifier"),
            let codeDataModels = decoder.decodeArrayOfObjects(ofClass: CodeDataModel.self, forKey: "codeDataModels")
        else {
            return nil
        }
        self.identifier = identifier as String
        self.codeDataModels = codeDataModels
    }
    func encode(with coder: NSCoder) {
        coder.encode(identifier, forKey: "identifier")
        coder.encode(codeDataModels, forKey: "codeDataModels")
    }
}

This is very like the first except I’m using decodeArrayOfObjects(ofClass:forKey:) to decode the array. This is nice for a number of reasons, not least of which is that it returns a Swift array of the right type.

Finally, here’s the test function to make sure that everything is working:

func test() throws {
    let dm1 = CodeDataModel(data: Data("Hello".utf8))
    let dm2 = CodeDataModel(data: Data("Cruel".utf8))
    let dm3 = CodeDataModel(data: Data("World!".utf8))
    let model = CodeReadModel(identifier: "greetings", codeDataModels: [dm1, dm2, dm3])
    let archive = try NSKeyedArchiver.archivedData(withRootObject: model, requiringSecureCoding: true)
    guard let model2 = try NSKeyedUnarchiver.unarchivedObject(ofClass: CodeReadModel.self, from: archive) else {
        fatalError()
    }
    print(model2)
    // -> <Test759746.CodeReadModel: 0x600002506440>
    print(model2.identifier)
    // -> greetings
    print(model2.codeDataModels)
    // -> [<Test759746.CodeDataModel: 0x600002505da0>, <Test759746.CodeDataModel: 0x600002506620>, <Test759746.CodeDataModel: 0x600002506640>]
    print(model2.codeDataModels.map { String(decoding: $0.data, as: UTF8.self) })
    // -> ["Hello", "Cruel", "World!"]
}

All of this was built with Xcode 15.4 and testing on macOS 14.5.

Share and Enjoy

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

Accepted Answer

Thank you for your kind assistance, The Eskimo. Your unarchivedObject(ofClasses:from:) doesn't even include NSDictionary, NSArray, NSString? The unarchivedObject guy is quite confusing.

I have minimized my work like the following to see how the guy works.

import Cocoa

class ViewController: NSViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func saveClicked(_ sender: NSButton) {
        showSavePanel()
    }
    
    @IBAction func openClicked(_ sender: NSButton) {
        showOpenPanel()
    }
    
    func showSavePanel() {
        let panel: NSSavePanel = NSSavePanel()
        ...
        if (panel.runModal().rawValue == NSApplication.ModalResponse.cancel.rawValue) {
            return
        } else {
            showSavePanelNext(fileURL: panel.url!)
        }
    }
    
    func showSavePanelNext(fileURL: URL) {
        let codeModel = CodeModel2(identifier: UUID().uuidString, highlightIndex: 1)
        NSKeyedArchiver.setClassName("CodeModel2", for: CodeModel2.self)
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: codeModel, requiringSecureCoding: false)
            try data.write(to: fileURL, options: .atomicWrite)
        } catch {
            print("Error: \(error.localizedDescription)")
        }
    }
    
    func showOpenPanel() {
        let panel = NSOpenPanel()
        ...
        if (panel.runModal().rawValue == NSApplication.ModalResponse.cancel.rawValue) {
            return
        } else {
            showOpenPanelNext(fileURL: panel.url!)
        }
    }
    
    func showOpenPanelNext(fileURL: URL) {
        do {
            NSKeyedUnarchiver.setClass(CodeModel2.self, forClassName: "CodeModel2")
            let data = try! Data(contentsOf: fileURL)
            if let dict = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSString.self, CodeModel2.self], from: data) {
                if let model = dict as? CodeModel2 {
                    let id = model.identifier
                    print("**** \(id)")
                }
            } else {
                print("What!?")
            }
        }
    }
}

class CodeModel2: NSObject, NSSecureCoding {
    class var supportsSecureCoding: Bool { true }
    
    let identifier: String
    let highlightIndex: Int
    init(identifier: String, highlightIndex: Int) {
        self.identifier = identifier
        self.highlightIndex = highlightIndex
    }
    required init(coder decoder: NSCoder) {
        self.identifier = decoder.decodeObject(forKey: "identifier") as! String
        self.highlightIndex = decoder.decodeInteger(forKey: "highlightIndex")
    }
    func encode(with coder: NSCoder) {
        coder.encode(identifier, forKey: "identifier")
        coder.encode(highlightIndex, forKey: "highlightIndex")
    }
}

It's a simple example, and it works. The following doesn't. dict doesn't return a value.

import Cocoa

class ViewController: NSViewController {
    func showSavePanelNext(fileURL: URL) {
        let codeDataModel = CodeDataModel(lastComponent: "hello.png", data: Data("World!".utf8))
        let codeModel = CodeReadModel(identifier: UUID().uuidString, highlightIndex: 2, codeDataModels: [codeDataModel])
        NSKeyedArchiver.setClassName("CodeReadModel", for: CodeReadModel.self)
        NSKeyedArchiver.setClassName("CodeDataModel", for: CodeDataModel.self)
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: codeModel, requiringSecureCoding: false)
            try data.write(to: fileURL, options: .atomicWrite)
        } catch {
            print("Error: \(error.localizedDescription)")
        }
    }
    
    func showOpenPanel() {
        let panel = NSOpenPanel()
        panel.allowsMultipleSelection = false
        panel.canChooseDirectories = false
        panel.canCreateDirectories = false
        panel.canChooseFiles = true
        panel.allowedContentTypes = [.text]
        panel.title = "Open me!"
        panel.message = ""
        //panel.directoryURL = NSURL.fileURL(withPath: openPath)
        if (panel.runModal().rawValue == NSApplication.ModalResponse.cancel.rawValue) {
            return
        } else {
            showOpenPanelNext(fileURL: panel.url!)
        }
    }
    
    func showOpenPanelNext(fileURL: URL) {
        do {
            NSKeyedUnarchiver.setClass(CodeReadModel.self, forClassName: "CodeReadModel")
            NSKeyedUnarchiver.setClass(CodeDataModel.self, forClassName: "CodeDataModel")
            let data = try! Data(contentsOf: fileURL)
            if let dict = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, NSArray.self, NSString.self, CodeReadModel.self, CodeDataModel.self], from: data) {
                if let model = dict as? CodeReadModel {
                    let id = model.identifier
                    print("**** \(id)")
                }
            } else {
                print("What!?")
            }
        }
    }
}

class CodeDataModel: NSObject, NSSecureCoding {
    class var supportsSecureCoding: Bool { true }
    
    let lastComponent: String
    let data: Data
    init(lastComponent: String, data: Data) {
        self.lastComponent = lastComponent
        self.data = data
    }
    required init(coder decoder: NSCoder) {
        self.lastComponent = decoder.decodeObject(forKey: "lastComponent") as! String
        self.data = decoder.decodeObject(forKey: "data") as! Data
    }
    func encode(with coder: NSCoder) {
        coder.encode(lastComponent, forKey: "lastComponent")
        coder.encode(data, forKey: "data")
    }
}

class CodeReadModel: NSObject, NSSecureCoding {
    class var supportsSecureCoding: Bool { true }
    
    let identifier: String
    let highlightIndex: Int
    let codeDataModels: [CodeDataModel]
    init(identifier: String, highlightIndex: Int, codeDataModels: [CodeDataModel]) {
        self.identifier = identifier
        self.highlightIndex = highlightIndex
        self.codeDataModels = codeDataModels
    }
    required init(coder decoder: NSCoder) {
        self.identifier = decoder.decodeObject(forKey: "identifier") as! String
        self.highlightIndex = decoder.decodeInteger(forKey: "highlightIndex")
        self.codeDataModels = decoder.decodeObject(forKey: "codeDataModels") as! [CodeDataModel]
    }
    func encode(with coder: NSCoder) {
        coder.encode(identifier, forKey: "identifier")
        coder.encode(highlightIndex, forKey: "highlightIndex")
        coder.encode(codeDataModels, forKey: "codeDataModels")
    }
}

Oops... I've changed

class CodeReadModel: NSObject, NSCoding {}

to

class CodeReadModel: NSObject, NSSecureCoding {}

The dict guy now returns a value.

I have minimized my work

Please continue that process to produce something truly minimal. In situations like this you should be able to create a simple command-line tool project that demonstrates your issue. That’s how I developer the example I posted earlier. Indeed, if you add this line to the bottom of my code:

test()

it’ll actually run in a command-line tool project.

Share and Enjoy

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

Unarchiving an object with custom classes
 
 
Q