Storing Persistent Values in Preferences

asobjc
(Mark Alldritt) #1

If any top-level variables in a script contain Cocoa values, you lose persistence of globals. In script bundles and applets you can use NSUserDefaults to store values in a standard preference property list file.

In the case of a .scptd file, you need to initialize defaults with a suite name that matches the file’s bundle id, like this:

use framework "Foundation"
use scripting additions

set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:(my id)

From then on you can simply read (using objectForKey:) or write (using setObject:forKey:) to your defaults.

For applets, it’s a bit trickier: applications normally use standarUserDefaults to access their defaults, but if you do that, when you edit and test you will be dealing with the preferences of the script editor, not the applet. You can cover all options like this:

use framework "Foundation"
use scripting additions
global theDefaults

if my id = missing value then error "This code works only in an applet or script bundle."
if current application's id = my id then -- must be running as applet
	set theDefaults to current application's NSUserDefaults's standardUserDefaults()
else -- being run by editor or other host app
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:(my id)
end if

This approach will work for both applets and bundle files. Using a global variable lets you access the defaults object easily from anywhere in your script.

It is obviously very important to ensure your files have unique bundle ids.

3 Likes

Mojave and AppleScript Applets
(Doeke Zanstra) #2

Good to know. This works with the defaults system, right?

I made some functions to work with do shell script "defaults read...", but this seams easier to work with.

0 Likes

(Shane Stanley) #3

This is the defaults system, yes. You use the above, and you can set your factory defaults using registerDefaults:, passing a record/dictionary of values.

0 Likes

(Jim Underwood) #4

Thanks for sharing this, Mark. It looks very useful.

Could anyone share an example of read/write of the defaults for noobs like me?
TIA.

0 Likes

(Shane Stanley) #5

Let’s say you want to store a number, which the user can change when the script runs. You might want a default of 1. So you’d use the above plus something like:

theDefaults's registerDefaults:{myNumber:1}

If you don’t want a default value, you can leave that line out.

To set a new value, you would use:

theDefaults's setObject:2 forKey:"myNumber"

And to read the value:

set theNumber to (theDefaults's objectForKey:"myNumber") as integer

When you read the value, you will get whatever was last set, but if it’s never been set you get the value you set using registerDefaults: (or missing value if you didn’t register a default).

There are some other methods specialised for things like URLs, but that’s the gist of it. You can only store values that can be stored in property lists (URLs get converted to data or strings).

1 Like

(Jim Underwood) #6

thanks, Shane. That’s very helpful. I now have a complete working model. :+1:

0 Likes

(Shane Stanley) #7

FYI, here are some prose and cons of using properties versus using defaults:

Properties
Pros:
Near automatic; simple to use
Store all non-ASObjC classes

Cons:
Can’t be used with code-signed apps
Fail (silently) in several circumstances: locked scripts, scripts on locked volumes, scripts with top-level ASObjC values, scripts that get interrupted
Can’t store Cocoa values
Results in changed modification date and size of scripts

Neither pro nor con:
Stores per-script

Defaults
Pros:
Can be used with code-signed scripts
Reliable – work on locked volumes, with ASObjC values, and unaffected by script interruptions
Values are transferrable
Some visibility
Can be shared

Cons:
Requires ASObjC
More complex to use, especially with non-bridged AS classes

Neither pro nor con:
Stores per-user

0 Likes

(Shane Stanley) #8

Coming back to this code…

There’s a potential issue where the id changes, such as during a save-as, or where you change it in the Resources panel. Unfortunately the system caches the initial value, and there seems to be no way of flushing it. So my id can potentially return an out-of-date result in an editor. (It’s a non-issue in applets, just when editing.)

The solution is not to rely on the id if it doesn’t match that of current application, but instead to extract it directly from the Info.plist file.

Here’s revised code to cover that situation:

use framework "Foundation"
use scripting additions
global theDefaults

if my id = missing value then error "This code works only in an applet or script bundle."
if current application's id = my id then -- must be running as applet
	set theDefaults to current application's NSUserDefaults's standardUserDefaults()
else -- being run by editor or other host app
	set myPath to POSIX path of (path to me)
	set theData to current application's NSData's dataWithContentsOfFile:(myPath & "Contents/Info.plist")
	set infoDict to current application's NSPropertyListSerialization's propertyListWithData:theData options:0 |format|:(missing value) |error|:(missing value)
	set myID to infoDict's objectForKey:"CFBundleIdentifier"
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:myID
end if

The extra code is only ever run when editing, so it has no effect on performance.

0 Likes

(Gary Gauthier) #9

I’m having a bit of difficulty just getting the initial code in posting #8 to run.

For some reason the system is triggering the error message, “This code works only in an applet or script bundle.”

I assume the note about it having to be in an applet or script bundle is because there wouldn’t otherwise be a plist to contain the id. Is this correct?

In the plist for the app containing the noted code, I see:
CFBundleIdentifier
com.apple.automator.Setting User Defaults

It seems like there is an identifier, though I’m not sure if this was what gets reflected in the id variable.

0 Likes

(Shane Stanley) #10

This is essentially due to a short-coming in Automator.

0 Likes

(Gary Gauthier) #11

Thank you. Sounds like Automator is really not up to some very basic tasks.

0 Likes

(Shane Stanley) #12

AppleScript is really just a bit player in the Automator world. It’s meant to be a way of building Automator actions, not complete workflows. As such, I think you need to temper your expectations. Different tools suit different tasks.

0 Likes

(Gary Gauthier) #13

Yes, I’m getting that impression about Automator as a coding platform.

I worked in the aerospace industry for years using Ada, strict data-typing and very good reference and style manuals. So; I suppose that I still have certain (possibly too high) expectations of coding platforms. The Automator “environment” definitely seems like an afterthought by Apple. Like you, I get the impression it was more likely geared to stringing together the pre-coded building blocks they ship with it, than doing serious line-by-line coding. Script Debugger definitely seems closer to what I would expect. So; I’m trying to convince “she who must be obeyed” to allow me to purchase Script Debugger. :>)

0 Likes