NSUserDefaults from any script

Normally NSUserDefaults are tied to a particular application: we call it using the ‘standardUserDefaults’ method, and it sets up a plist file using the app’s bundle id. However, I realized today that we can access the defaults mechanism from any script, without a bundle id, by using the ‘initWithSuiteName’ initializer. In a few ASOC handlers:

use framework "Foundation"

property ca : current application
property NSUserDefaults : class "NSUserDefaults"
property defaultsSuite : missing value
property suiteID : "user.script.defaultsTest"

on init()
	set defaultsSuite to NSUserDefaults's alloc's initWithSuiteName:suiteID
end init

to setValueForKey(keyName, value)
	if defaultsSuite = missing value then my init()
	defaultsSuite's setObject:value forKey:keyName
end setValueForKey

to getValueForKey(keyName)
	if defaultsSuite = missing value then my init()
	return defaultsSuite's objectForKey:keyName
end getValueForKey

to setSuiteValues(keyValueRecord)
	-- this will erase any previous data and create a new plist
	NSUserDefaults's alloc's setPersistentDomain:keyValueRecord forName:suiteID
end setSuiteValues

to getSuiteValues()
	return (NSUserDefaults's alloc's persistentDomainForName:suiteID)
end getSuiteValues

to coerceObjToAS(obj)
	-- coercion handler to get things back to applescript-frienly values
	 return item 1 of (current application's NSArray's arrayWithObject:obj) as list
end coerceObjToAS

Call it like so:

setSuiteValues({alpha:{1, 2, 3}, beta:3.1415926})
setValueForKey("beta", 1.414213562373)
setValueForKey("gamma", true)
coerceObjToAS(getSuiteValues())

You can make a library script out of this, and use it for persistent storage whenever applescript’s built-in property persistence doesn’t work. You can even use it to share persistent data between different scripts, or to examine or modify the preference plists of applications.

Edit: Added in Shane’s ‘coerceObjToAS’ handler from his post below, because it’s much simpler than my original version.

Can someone explain to me what the difference is between this:

property NSUserDefaults : class "NSUserDefaults"

and this:

property NSUserDefaults : a reference to NSUserDefaults of ca

Is there a difference in the speed of access to the property, and can there be any error when applying the first (short form used by TedW)?

NOTE: Your post is just great, TedW! The plist file with proper keys successfully created in Library folder’s Preferences folder of home directory.

You sure can: https://www.macosxautomation.com/applescript/apps/Script_Libs.html#PrefsStorageLib

:wink:

That actually uses the bundleID, so it’s not quite the same…

You’re missing dates. And if you use it as a general-purpose converter, it’s missing files, and will also hit problems with large integers. I reckon this is a bit simpler:

to coerceObjToAS(obj)
    return item 1 of (current application's NSArray's arrayWithObject:obj) as list
end coerceObjToAS

An interesting question! Here’s some experimentation I’ve been doing using NSString:

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

-- Experimenting with NSString.
property classNSString : class "NSString"
property classNSStringRef : a reference to class "NSString"
property caClassNSString : current application's class "NSString"
property caClassNSStringRef : a reference to current application's class "NSString"
-- prop NSString : current application's NSString -- Doesn't compile.
property NSStringRef : a reference to current application's NSString

-- What's reported to be in these properties?
{classNSString, classNSStringRef, caClassNSString, caClassNSStringRef, NSStringRef}
--> {class "NSString", class "NSString", class "NSString", class "NSString", NSString} (In Script Debugger)
--> {class "NSString", class "NSString" of «script», class "NSString", class "NSString", NSString} (In Script Editor)

-- Are all five ultimately references to the NSString class?
{classNSString's contents, classNSStringRef's contents, caClassNSString's contents, caClassNSStringRef's contents, NSStringRef's contents}
--> {(Class) NSString, (Class) NSString, (Class) NSString, (Class) NSString, (Class) NSString} -- (In Script Debugger)
--> {«class ocid» id «data optr00000000987B99A9FF7F0000», «class ocid» id «data optr00000000987B99A9FF7F0000», «class ocid» id «data optr00000000987B99A9FF7F0000», «class ocid» id «data optr00000000987B99A9FF7F0000», «class ocid» id «data optr00000000987B99A9FF7F0000»} (In Script Editor)

-- What actually works (in an editor)?
{(classNSString's stringWithString:("Hello")) as text, ¬
	((get classNSStringRef's contents)'s stringWithString:("Hello")) as text, ¬
	(caClassNSString's stringWithString:("Hello")) as text, ¬
	(caClassNSStringRef's stringWithString:("Hello")) as text, ¬
	(NSStringRef's stringWithString:("Hello")) as text, ¬
	((get class "NSString")'s stringWithString:("Hello")) as text, ¬
	(current application's class "NSString"'s stringWithString:("Hello")) as text, ¬
	(current application's NSString's stringWithString:("Hello")) as text}
--> {"Hello", "Hello", "Hello", "Hello", "Hello", "Hello", "Hello", "Hello"}

It looks as if class “NSString” — provided it’s ‘got’ first and the “Foundation” framework is ‘used’ — becomes equivalent a reference to that class of the current application. In other words, both it and a reference to current application’s NSString are effectively stored references to the class. As such, there’s probably little difference between them for speed when they’re used. Shane will know better than I do whether or not class “className” by itself, without an explicit reference to the current application, is legitimate in ASObjC code.

In vanilla AppleScript, a reference held in a variable usually takes very slightly longer to follow than one written directly into the source code and I imagine the same’s true here. But you’re unlikely to notice the difference in practice.

Before AppleScriptObjC first appeared, as an Xcode-only thing in 10.6, AppleScript didn’t support referring to classes by name at all. It still doesn’t typically – you can’t use something like as class “text”.

ASObjC in Xcode required a way of specifying a subclass, via the parent property. But clearly AppleScript couldn’t provide terminology for every conceivable Cocoa class name, so something was fiddled somewhere to allow the use of a string. I’m not sure what they did, because initially neither Script Editor nor Script Debugger could actually compile code that used it (Xcode included its own AppleScript compiler built-in in those days). I faced the same issue when writing ASObjC Explorer. The solution was to add a hidden class definition to the app’s own scripting dictionary (all three apps did this, with slight variations). I haven’t checked if that’s still needed today, but I confess that the effort of tracking down the problem colored my attitude to the notion :frowning: .

Using a string without current application is interesting. It works fine in a property, but it gets ugly elsewhere – you need to use something like:

set y to (get class "NSString")'s stringWithString:"blah"

About the only thing that form has going for it is that if you mis-spell the name, the error message is arguably more useful.

But I guess this inconsistent behavior makes me reluctant to use it; it’s not as if it’s that big a time-saving.

I did ask an AppleScript engineer whether there was a difference between using a string and a variable for class names generally, and the answer I got was the variable form was the style used at that time, but he gave no reason or further explanation. I suspect it’s largely a matter of taste, and the fact that in many languages, especially C, the use of strings is generally discouraged for the same reason enums and consts are used: to allow compiler checking.

As for speed differences, I suspect we’re talking background noise at most.

OoooOOooo… I missed that one last time I looked at that page. :slight_smile:

I got that form for this page at Apple Developer: https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/AppendixA-AppleScriptObjCQuickTranslationGuide.html. Down in the ‘Classes and Constants’ section, it says:

So it seems to me this is just a language convention, not a different reference form. It might have a minuscule impact at compile time, but not at run time.

It is that… I would edit that in to my original post, but I’m no sure what the MacScripter convention is: Edit it in? Create a new version? Leave your post as sufficient?

That’s entirely up to you. :slight_smile: A reasonable course would be to make the edit you want in your original post and add a note underneath the script saying something like Edit: coerceObjToAs() handler modified in the light of Shane’s suggestion below.” Or however you want to phrase it.

Thanks for the background, Shane! :slight_smile:

Thanks all for reply to my question.

FWIW, I had a discussion with one of the engineers about using my instead, and he was adamant that it was a bad idea. I believe at the time there was still an ambition to try to modify the compiler so current application would not be needed at all, and I got the impression this might affect existing scripts that used my instead.

Of course we can assume that’s all academic now. But it seems to me that using current application sometimes and my at other times is likely to cause a bit of head scratching, to say the least.