Encoding and Decoding Objects out of date code?

Just making my way through a RW video tutorial on Encoding and Decoding Objects (https://www.youtube.com/watch?v=V9OmySqbBK4) to find that at long last after I got my class to conform that the code it is now out of date syntax and I don't know if what I did was a waste of time or not?

class CourseItem: NSObject, NSCoding {

    var objectType: String = ""
    var objectSFX: String = ""

    override init() {
        super.init()
    }
    
    required convenience init?(coder aDecoder: NSCoder) {
        guard let objectType = aDecoder.decodeObject(forKey: "objectType") as? String,
            let objectSFX = aDecoder.decodeObject(forKey: "objectSFX") as? String else { return nil }
        
        self.init(objectType: objectSFX, objectSFX: objectSFX)
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(objectType, forKey: "objectType")
        coder.encode(objectSFX, forKey: "objectSFX")
    }
    
    init(objectType: String, objectSFX: String) {
        
        super.init()
        self.objectType = objectType
        self. objectSFX = objectSFX
    }

I hope the above is still relevant in Swift v5x because it took me a long time to figure out and I have multiple class inheritance, so it got complicated. However, the following is where I got stuck and I wonder now what has changed since 2017:

    func setupItems() {
        
        let testItem = CourseItem()
        
        if let dataURL = Bundle.main.url(forResource: "ItemDatabase", withExtension: "plist") {
            if let plistData = NSData(contentsOf: dataURL) {
                var format = CFPropertyListFormat.xmlFormat_v1_0
                
                do {
                    let plist = try NSPropertyListSerialization.propertyList(from: plistData as Data, options: .mutableContainersAndLeaves, format: format)
                    
                    let itemData = plist["Items"]! as! [AnyObject]
                    
                    for item in itemData {
                        
                        let objectType = item["objectType"] as! String
                        let objectSFX = item["objectSFX"] as! String

                        let item = CourseItem(objectType: objectType, objectSFX: objectSFX)
                    }
                } catch {
                    print(error.localizedDescription)
                }
            }   
        }   
    }

I get an error at the point of PropertyListSerialization: Cannot convert value of type 'CFPropertyListFormat' to expected argument type 'UnsafeMutablePointer<PropertyListSerialization.PropertyListFormat>?' and am unsure how best to proceed as I have yet to touch FileManager and the like, ultimately to populate levels with item data from a plist, as well as to save data.

How out of date is my code please?

The last parameter to NSPropertyListSerialization.propertyList(from:options:format:) is supposed to be an out parameter that tells you which format is actually found in the Data you passed. If you don’t care about that (and you probably don’t), you can pass nil there instead.

1 Like

Hi brextdax, thank you for your reply.

Originally I did pass in a nil value, but, as now, got the "Value of type 'Any' has no subscripts" when when assigning the plist values to the variable on the following lines. After trying a number of different things I actually forgot about that.

More importantly though, in order to assign data to objects from a plist, am I going about it the right way do you know? I hear that parsing JSON and the like can be done in just one line, but I don't see an easier way to encode/decode than what I have. If however the setup function contains old concepts fine, but that is then where I need focus next.

You could use codable to encode and decode objects.

You could use a PropertyListEncoder / PropertyListDecoder

Refer:

1 Like

How out of date is my code please?

Very out of date, alas )-:

Here’s a version of your code that compiles:

func setupItems() {

    let testItem = CourseItem()

    if let dataURL = Bundle.main.url(forResource: "ItemDatabase", withExtension: "plist") {
        if let plistData = NSData(contentsOf: dataURL) {
            do {
                let plist = try PropertyListSerialization.propertyList(from: plistData as Data, options: .mutableContainersAndLeaves, format: nil)
                let plistAsDict = plist as! [String:Any]
            
                let itemData = plistAsDict["Items"]! as! [AnyObject]
            
                for item in itemData {
                
                    let objectType = item["objectType"] as! String
                    let objectSFX = item["objectSFX"] as! String

                    let item = CourseItem(objectType: objectType, objectSFX: objectSFX)
                }
            } catch {
                print(error.localizedDescription)
            }
        }
    }
}

I didn’t try running it, but I expect it’ll work. The main differences are:

  • I got rid of format, for the reasons explained by Brent.

  • I added code to convert plist to plistAsDict, which is necessary for the later subscripting.

However, somu has the right answer here. Switching to Codable will make your code much better (that is, both smaller and safer). Assuming your database looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>items</key>
    <array>
        <dict>
            <key>objectType</key>
            <string>a</string>
            <key>objectSFX</key>
            <string>b</string>
        </dict>
        <dict>
            <key>objectType</key>
            <string>c</string>
            <key>objectSFX</key>
            <string>d</string>
        </dict>
        <dict>
            <key>objectType</key>
            <string>e</string>
            <key>objectSFX</key>
            <string>f</string>
        </dict>
    </array>
</dict>
</plist>

you can parse it with this code:

struct CourseItem: Codable {
    var objectType: String
    var objectSFX: String
}

struct Database: Codable {
    var items: [CourseItem]
}

func loadDatabase() throws -> Database {
    let url = Bundle.main.url(forResource: "ItemDatabase", withExtension: "plist")!
    let data = try Data(contentsOf: url)
    let decoder = PropertyListDecoder()
    return try decoder.decode(Database.self, from: data)
}

Neat-o!

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

3 Likes

Hello eskimo, many thanks for sorting that out for me. I can't believe the mountain of code that I was able to remove because of this conformance. Cutting back to something simpler like struct over a class, as I really didn't need the inheritance, made it even easier!

    func setupTable() {
        
        do {
            let item = try loadDatabase()
            
            for forks in item.cutlery {
                print(forks.brand)            
            
            for plates in item.tableware {
                print(plates.brand)            
            }
        } catch {
            print(error)
        }
    }

For my needs I had the impression that using a plist for the data I would also use it to save the data, but am having trouble with it:

        let tableDatabasePath = Bundle.main.path(forResource: "TableDatabase", ofType: "plist")
        
        let url = URL(fileURLWithPath: tableDatabasePath!)

        let tableDictionary = NSMutableDictionary(contentsOfFile: tableDatabasePath!)

        let missingCutleryArray = tableDictionary?.object(forKey: "missingCutlery") as! NSMutableArray

        missingCutleryArray[0] = "spork"

        tableDictionary?.write(to: url, atomically: true)

My XML (plist) is nested, so I tried (as in the first example) to loop through the elements, but it didn't work, so it couldn't find the entry. However, even on the first item of the array (which was found) the entry was not changed.

I am unsure about whether I have the correct model or approach. I am content to load from a plist, but do those initial values get edited, or are they stored somewhere else and override the defaults if present, how does it work?

Also, when updating the app, how do I ensure that I don't reset custom user data, I am concerned about that and how it works.

Thanks again !

Thanks Somu, I'd actually checked those already. Good documentation, but needed more practical help.

OK, let’s start with a minor nit. You don’t need to do this:

let tableDatabasePath = Bundle.main.path(forResource: "TableDatabase", ofType: "plist")
let url = URL(fileURLWithPath: tableDatabasePath!)

Bundle has a url(forResource:withExtension:) method that gives you back a URL.

Next, you definitely will not be able to write the database back to your app’s bundle. That is read-only. If you’re working on an iOS app, you’d typically write your data to the Documents directory, which you can get with url(for:in:appropriateFor:create:).

when updating the app, how do I ensure that I don't reset custom user
data

That is a tricky problem. There’s basically two strategies for dealing with changes:

  • You can copy the default database, apply the change to that, save that, and that now becomes the user’s data (A)

  • You can record user changes relative to the default database (they added this, they deleted that, the modified this) (B).

These have there various pros and cons. The main benefit to A is that it’s simple.

Finally, when it comes to modifying the data, I recommend against monkeying with with the various property list types. Rather, bring the database into memory as a model object (like the Database struct from my previous response), edit that, and then write it back. Remember that Codable goes both ways: You can decode a property list to a model type, but you also encode a model type to a property list.

IMPORTANT This strategy assumes you database is small enough to easily fit in memory. If not, things get more complex.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple