Beyond Scripting in Swift: Reading and writing property lists

The most popular occasion for using Property Lists is, of course, for preference files, and Swift document-based apps make that straightforward, as I have described elsewhere.

When I thought about the file format for DispatchRider, it quickly became clear that Property Lists were a good option, because of their ease of conversion to and from Dictionaries. And the most obvious data structure to use internally for the settings of an activity in DispatchRider is the Dictionary.


My first job was then to convert my ViewController code to work with a Dictionary containing the various settings. This is declared as:
var SetsDict = [ : ] as [String : Any]
and the individual settings were stripped out.

There are a couple of occasions where the view controls need to be updated from the values held in that Dictionary, so I added a new function to do that:
func dictUpdate() {
if ((SetsDict["Repeat"] as! Int) == 1) {
repeatCheck.state = NSOnState
} else {
repeatCheck.state = NSOffState
qosPopup.selectItem(at: (SetsDict["QoS"] as! Int))
commandText.stringValue = SetsDict["Command"] as! String
paramsText.stringValue = SetsDict["Params"] as! String
repeatNum.stringValue = "\(SetsDict["Period"] as! Int)"
toleranceNum.stringValue = "\(SetsDict["Tolerance"] as! Int)"

The viewDidLoad() function required a bit more care, because it is called when opening a window on a new document, at which time the Dictionary is empty, and when opening an existing document, and the Dictionary has been read in from the saved file. So it first does the usual view bits, including setting up the popup menu:
override func viewDidLoad() {
qosPopup.addItems(withTitles: QoSlist)

The next step is to check whether this is a new document with an empty Dictionary. If it is, it needs to be populated with defaults (in a later version we might read those from a Preferences file, perhaps):
if (SetsDict.isEmpty) {
SetsDict["Command"] = "/usr/local/bin/blowhole"
SetsDict["Params"] = ""
SetsDict["Repeat"] = 1
SetsDict["Period"] = 60
SetsDict["Tolerance"] = 0
SetsDict["QoS"] = 0

Those are shown below.


Then it updates the controls using that Dictionary:
countText.stringValue = "\(result)"
stopButtOut.state = NSOffState


Another important step is to ensure that the Dictionary is kept in synchrony with controls, so each text box and other changeable control needs an Action such as:
@IBAction func repeatTextAct(_ sender: Any) {
SetsDict["Period"] = repeatNum.integerValue

Having prepared the ViewController, the final step is to add the code required to read and write the Property Lists, in the Document. We first need to make an empty Dictionary which we can access here too:
var theDict = [:] as [String : Any]

The function makeWindowControllers() needs to check whether that dictionary is empty, in which case it is dealing with a new document, or whether it has read in a Dictionary from a file. In the latter case, it passes the Dictionary and calls dictUpdate() to load the saved data into the controls:
override func makeWindowControllers() {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as! NSWindowController
let theVC = self.windowControllers[0].contentViewController as? ViewController
if !(theDict.isEmpty) {
theVC?.SetsDict = theDict

This leaves just the write and read functions to code. Although there are other ways of going about this, the presence of NSDictionary methods to initialise from and write to a URL led me to override the URL-based methods.

So to write the Dictionary out to a URL:
override func write(to url: URL, ofType typeName: String) throws {
if let theVC = self.windowControllers[0].contentViewController as? ViewController {

We need to use the toll-free bridging between Swift’s Dictionary and NSDictionary explicitly here:
let theNSDict = theVC.SetsDict as NSDictionary

Then write the Dictionary to the URL and provide the throw:
theNSDict.write(to: url, atomically: true)
} else {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)

Reading is more straightforward, although I had wanted to check the typeName, to ensure that this is a Property List. Sadly that doesn’t seem to be particularly useful, so I ended up just checking that there is a non-empty typeName:
override func read(from url: URL, ofType typeName: String) throws {
if (typeName != "") {
theDict = NSDictionary.init(contentsOf: url) as! [String : Any]
} else {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)

This is formatted better below.


The one strange bug which remains is that, when saving, this will overwrite an existing file without any warning. I am not sure why that is, although it might of course stem from the fact that I am using URL-based saving rather than file-specific calls.