swift 5: using plists to store data

Using plists to store data in Swift doesn’t get much attention on the web. Although not the most elegant (or recommended) solution to store data, reading from and writing to property lists is a convenient way to store small bits of data. For example, simple plists can be used to store level data for games.

In the image above, we’ve generated a level set using an external program (think something like python or even Excel). The plist can be dragged-in to any Xcode project and referenced via a few simple lines of code.

Note: you cannot modify a plist included in the main bundle (i.e. dropped into the project as a resource). You’ll need to copy the file over into the users’ documents directory.

Modifying the plist is pretty straightforward. First, check to see if the plist exists in the documents directory. If not, copy it over, otherwise, reference the file.

func reloadData() -> Bool{
        if (try? levelDataURL().checkResourceIsReachable()) == nil {
            guard let originalFile = Bundle.main.url(forResource: Constants.kLevelDataFileName, withExtension: ".plist") else {
                fatalError("Could not locate level data")
            }
            
            do {
                let originalContents = try Data(contentsOf: originalFile)
                try originalContents.write(to: levelDataURL(), options: .atomic)
                print("Made a writable copy of the level data at \(levelDataURL())")
                return reloadData()
            } catch {
                print("Couldn't copy level data to documents directory")
                return false
            }
        } else {
            if let data = try? Data(contentsOf: levelDataURL()) {
                let decoder = PropertyListDecoder()
                do {
                    levels = try decoder.decode([Level].self, from: data)
                } catch {
                    print("Error loading level data")
                    return false
                }
            }
        }
        return true
    }

You’ll notice a [Level].self reference inside the decoder.code function. In order to decode the property list, you’ll need to define a class that represents the object in the list. Level is the Codable class that can be mapped to the plist.

class Level: Codable {
    var number: Int = 0
    var completed: Bool = false
    var available: Bool = false
    var score: Int = 0
    var speed: Float = 0.0
    var radius: Float = 0.0
    var feedspeed: Int = 0
    var path: [Int] = []
    var time: Int = 0
    var music: String = ""
    var musicStartTime: Int = 0
    var specials: [String] = []
    var orbital_types: [String] = []
}

Now that we’ve successfully copied over the plist and created an object that describes the each element in the plist array, we can also modify this copy.

       let encoder = PropertyListEncoder()
        
        do {
            guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last else {
                fatalError("Documents directory not found in application bundle.")
            }
            let url = documentsDirectory.appendingPathComponent("\(Constants.kLevelDataFileName).plist")
            print(url)
            let data = try encoder.encode(newLevels)
            try data.write(to: url)
        } catch {
            print("Error saving results: \(error)")
        }

You’ll notice a newLevels reference in the encoder.encode function. This is an array of Level objects that conform to the plist format. You’ll be required to save the whole plist any time you make changes (even to a single element). It should be pretty apparent why this is pretty clunky and not recommended, however, it’s a convenient way to store small chunks of data that don’t need to be modified often.

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.