Saving a mutable dictionary to a property list file

I’m optimizing an old script and have encountered an issue which I’ve resolved but not necessarily well. The script is long, so I’ll summarize.

I create a mutable dictionary using:

set bookmarkData to (current application's NSMutableDictionary's new())

I then add items to the dictionary using:

bookmarkData's setObject:currentPath forKey:currentName

Next I save the dictionary to a plist file using:

on writePlist(thePlist, theDictionary)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:thePlist
	set theDefaults's theKey to theDictionary
end writePlist

The next time I run the script the dictionary is read with:

on readPlist(thePlist)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:thePlist
	set theDictionary to theDefaults's theKey
	return theDictionary
end readPlist

The issue I’ve encountered is that the dictionary returned by the readPlist handler is no longer mutable. I say that because I get an unrecognized selector error from the setObject method. I fixed this by rewriting the readPlist handler as shown below.

on readPlist(thePlist)
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:thePlist
	set theDictionary to theDefaults's theKey
	return (theDictionary's mutableCopy())
end readPlist

My question is a simple one–why doesn’t the first-above readPlist handler return a mutable dictionary. The script with the second-above readPlist handler works fine and I guess I should accept that as good, but I couldn’t help but wonder. Thanks for the help.

Fredrik71. Thanks for looking at my thread.

In his book, Shane has a whole chapter dedicated to “Persisting Preferences” and this chapter appears to discuss doing exactly what I am doing. The dictionary saved to the plist file is used in my script only and has nothing to do with any other apps. I probably should have been clearer on this point.

BTW, the actual script I am working to optimize will be found in the last post of the following thread. In my testing, I have determined that NSMutableDictionary’s dictionaryWithContentsOfFile is appreciably slower than NSUserDefaults at this particular task.

https://macscripter.net/viewtopic.php?id=46437

Fredrik71. I see what you’re saying now, and that could be the answer. Many thanks.

Shane doesn’t address this issue directly in his book but he does state:

Perhaps mutable dictionaries cannot be saved.

I did a little addition research, and the documentation for NSUserDefaults contains the following paragraph, which appears to confirm the above:

Also, Shane’s book contains the following example, which seems to confirm that mutableCopy is the correct method to use to make the dictionary mutable after retrieval by NSUserDefaults:

use framework "Foundation"

on testIt()
	set anNSArray to current application's NSArray's arrayWithArray:{1, 2, 3}
	set newArray to anNSArray's mutableCopy()
	newArray's addObject:4
	return newArray as list
end testIt

testIt()

It looks like you’ve answered your own question, but to go a little further, you can use NSPropertyListSerialization when reading property lists (not via defaults) to control mutability.

For example:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set theData to current application's NSData's dataWithContentsOfFile:thePath
set {theDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:theData options:(current application's NSPropertyListMutableContainersAndLeaves) format:(missing value) |error|:(reference)

Thanks Shane. I placed the your code in my script and it worked fine. I’m not familiar with NSData or NSPropertyListSerialization, and I’ll work to learn those.

Just by fumbling around a bit, I got the following to work to write the plist file. Other than error correction, which I’ll add, does this approach look OK? I use this script a lot and want to make sure. Thanks again.

on writePlist(thePath, theDictionary) -- theDictionary is a mutable dictionary and thePath is POSIX path
	set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:theDictionary format:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference)
	set theResult to theData's writeToFile:thePath atomically:true
end writePlist

Sure. You can also use -writeToURL:error: (since 10.13) or the older -writeToFile:atomically:/writeToURL:atomically: directly from the dictionary.

Thanks Shane. I appreciate the help.

All of my questions have been answered and I thought I would add a postscript.

I ran timing tests with three approaches that can be used to write a mutable dictionary to a property list file and to read it afterwards. The test dictionary contained 300 keys (which is a worse-case scenario for me) and the tested options were:

  1. NSMutableDictionary
  2. NSUserDefaults
  3. NSData (NSPropertyListSerialization)

The write times were measured with a timing script and the read times with Script Geek. The end result was that all three options were reasonably quick with write times of 12 milliseconds or less and read times of 8 milliseconds or less. The read times, which were of greatest interest to me, generally varied by no more than 2 milliseconds.

I state in an earlier post that NSMutableDictionary was significantly slower than NSUserDefaults. That is not correct and was the result of an error in my testing.

Just wanted to say that on some versions of SDKs and Applescripts
I’ve had issues trying to save NSURL’s in a dict or Using Serialization and NSArchiver.
I had to get the path (NSString) then store that as a value in the dictionary.
And on the reverse side, recreate the NSURL’s from the “saved” NSString.

I believe it may have been in OS10.11 that this wasn’t an issue.

So if you ever have other issues when trying to save.
Remember the basic NSObjects that it can encode.
Things like enums, NSPoint, NSRect etc need to be converted to NSValue
And then stored in the Dictionary as a value for Key.
Then also when un archiving the reverse must be done.
Some of the Classes have convince methods for creating the NSValue and also for i it the Class with a NSValue. (Rather than found thru NSData).

  • (NSValue *)valueWithRect:(NSRect)rect;

property(readonly) NSRect rectValue;

The bridging of NSURL and files/aliases was introduced in 10.11 (as was bridging between NSDate and AS dates).

Points and rects are probably better stored as strings. The functions to use are: NSStringFromRect(), NSRectFromString(), NSStringFromPoint() and NSPointFromString(). The result is a lot more human-readable.