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!

swift 5: memory leaks and closures

While certainly nothing compared to conventional memory management, one frustrating aspect of finishing up your iOS app is discovering runaway memory issues.

One primary source is with respect to closures. From the Swift Programming Reference Manual:

Closures are self-contained blocks of functionality that can be passed around and used in your code…

Closures can capture and store references to any constants and variables from the context in which they are defined. This is known as closing over those constants and variables. Swift handles all of the memory management of capturing for you.

https://docs.swift.org/swift-book/LanguageGuide/Closures.html

This last bolded-statement is technically correct, but not without caution. Closures can capture and store references. The implication being a possible Automatic Referencing Counting Cycle. Put simply, Swift can inadvertently create a referencing loop between the object passed to the closure and the object calling the closure. Below is a diagram illustrating the point:

We’ve run into this issue when using SpriteKit and SKActions. Take for example, the following function:

func run(_ toPoint: CGPoint speed: TimeInterval) {
        let move = SKAction.move(to: toPoint, duration: speed)
        self.run(move) {
            self.jump()
        }
    }

In many cases, calling self.jump() will result in a memory leak given the strong reference. The solution to this memory leak:

func run(_ toPoint: CGPoint speed: TimeInterval) {
        let move = SKAction.move(to: toPoint, duration: speed)
        self.run(move) {
            [weak self] in
            guard let strongSelf = self else { return }
            strongSelf.jump()
        }
    }

And there you have it. A simple solution to a nagging issue we all face at some point. Hope this helps.