NSUserDefaults not getting preference values from the preferences cache? - objective-c

I'm working with NSDefaults and trying to read the list of finder favorites in the finder. This code returns that list in a command line utility:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
#autoreleasepool {
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] init];
[userDefaults addSuiteNamed:#"com.apple.sidebarlists"];
NSDictionary *list = [userDefaults dictionaryForKey:#"favoriteitems"];
NSLog(#"%#", list);
}
return 0;
}
Unfortunately, the returned list seems to be stale.
For example, if I delete an entry from the finder favorites list via the finder and then rerun my little bit of code, the deleted entry remains in the list. This appears to be true until the system syncs the UserDefaults cache with the disk (I can force this by restarting the computer, for example).
Is there a way to force cocoa to read from the preferences cache and not the disk as it appears to be doing?

Don't do it this way. There's a supported API for this, although it's only documented in the headers. See the LSSharedFileList API in /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Headers/LSSharedFileList.h. You probably want the list with the identifier kLSSharedFileListFavoriteItems.

You can force synchronization with the synchronize method. Whether that will fix your issue i don't know.

Related

How to use NSPasteboard with kPasteboardTypeFileURLPromise for copy/paste?

My application would like to add a promise to the pasteboard for a file that is stored remotely, and may never be pasted—similar to pasting a file copied from a session controlling a VM or other remote system. Ideally, a user can paste in a Finder folder (or the desktop) and the promise would trigger and away we go. I am willing to deal with the issues of fulfilling the promise once triggered, but I have been unable to get the promise to trigger.
All of the promise code I have found deals with drag and drop, which is not functionality what I need (though it is possible that something from DnD needs to be in place for promises to work?)
I have tried using NSFilePromiseProvider with a delegate, and adding that to the pasteboard. I can see the entries on the pasteboard using a clipboard viewer, but when I paste in Finder nothing happens and no delegate methods are called. I can trigger the delegate methods by having the clipboard viewer access the entries, so I know that much is hooked up.
#interface ClipboardMacPromise : NSFilePromiseProvider<NSFilePromiseProviderDelegate>
{
NSString* m_file;
}
#end
#implementation ClipboardMacPromise
- (id)initWithFileType:(NSString*)type andFile:(NSString*)file
{
m_file = file;
return [super initWithFileType:type delegate:self];
}
- (NSString *)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString *)fileType
{
return m_file;
}
- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider writePromiseToURL:(NSURL *)url completionHandler:(void (^)(NSError * _Nullable errorOrNil))completionHandler
{
// Finder can't paste, so we never get here...
}
#end
NSPasteboard* pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
NSMutableArray* items = [[NSMutableArray alloc] init];
ClipboardMacPromise* promise = [[ClipboardMacPromise alloc] initWithFileType:(NSString*)kUTTypeFileURL andFile:#"dummy.txt"];
[items addObject:promise];
[pboard writeObjects:items];
I have also tried NSPasteboardItem with NSPasteboardItemDataProvider where I setup a promise for content on kUTITypeFileURL. It provided very similar entries on the pasteboard, but still no action when I paste in finder. Clipboard viewer will again trigger the provider fine when accessing the individual pasteboard entries. (NSPasteboard's declareTypes:owner: has the same behavior)
#interface ClipboardMacPromise : NSPasteboardItem<NSPasteboardItemDataProvider>
{
NSString* m_file;
}
#end
#implementation ClipboardMacPromise
- (id)initWithFile:(NSString*)file
{
m_file = file;
id _self = [super init];
if (_self) {
[_self setDataProvider:_self forTypes:#[(NSString*)kPasteboardTypeFileURLPromise]];
[_self setString:(NSString*)kUTTypeFileURL forType:(NSString*)kPasteboardTypeFilePromiseContent];
}
return _self;
}
- (void)pasteboard:(NSPasteboard *)pasteboard item:(NSPasteboardItem *)item provideDataForType:(NSPasteboardType)type
{
// we don't get here when we paste in Finder because
// Finder doesn't think there's anything to paste
// but using a clipboard viewer, we can force the promise to
// resolve and we do get here
}
#end
NSPasteboard* pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
NSMutableArray* items = [[NSMutableArray alloc] init];
ClipboardMacPromise* promise = [[ClipboardMacPromise alloc] initWithFile:#"file:///tmp/dummy.txt"];
[items addObject:promise];
[pboard writeObjects:items];
And for completeness, here is my Carbon attempt since Pasteboard.h seems to detail how this should work in a copy/paste scenario... but it still does not provide Finder what it is looking for. The generated clipboard entries look very similar between the three implementations.
OSStatus PasteboardPromiseKeeperProc(PasteboardRef pasteboard, PasteboardItemID item, CFStringRef flavorType, void * _Nullable context)
{
// 6) The sender's promise callback for kPasteboardTypeFileURLPromise is called.
string s = "dummy.txt";
CFDataRef inData = CFDataCreate(kCFAllocatorDefault, (UInt8*)s.c_str(), s.size());
PasteboardPutItemFlavor(pasteboard, item, flavorType, inData, 0);
return noErr;
}
PasteboardRef p = NULL;
PasteboardCreate(kPasteboardClipboard, &p);
PasteboardClear(p);
PasteboardSetPromiseKeeper(p, &PasteboardPromiseKeeperProc, this);
// 1) The sender promises kPasteboardTypeFileURLPromise for a file yet to be created.
PasteboardPutItemFlavor(p, (PasteboardItemID)1, kPasteboardTypeFileURLPromise, kPasteboardPromisedData, 0);
// 2) The sender adds kPasteboardTypeFilePromiseContent containing the UTI describing the file's content.
PasteboardPutItemFlavor(p, (PasteboardItemID)2, kPasteboardTypeFilePromiseContent,CFStringCreateExternalRepresentation(NULL, kUTTypeFileURL, kCFStringEncodingUTF8, 0), 0);
It really seems that there is a certain UTI that Finder is looking for on the pasteboard, and I don't have it. If I put a kUTTypeFileURL directly on the clipboard, it appears that finder actually checks for the existence of the file (ie. triggers Catalina's Desktop access prompt) before offering it to paste.
Does anyone know if or how file promises can be provided to Finder through Copy/Paste instead of Drag-and-Drop?
It appears that the key piece here is that Finder requires that the file actually be present on disk for the paste action to be enabled for a file URL. This one detail rules out the possibility of promises working for copy/paste -- at least with Finder.
The correct solution therefore requires a virtualized file system (like FUSE) so that the promises can be made and fulfilled at the filesystem level. Thus a collection of temporary zero-length files can be written to disk, and actual file URLs be added to the pasteboard. This fulfills the requirements that Finder has to enable paste. Then when a paste action is made, the file data is read from the virtualized file system which can in turn retrieve the actual data from the remote system. Finder is none the wiser. The copy will even have a built in progress bar!
It appears that Microsoft's Mac RDP client mostly works this way, although I was only ever able to get it to copy zero length files so this may be harder to get right than it sounds.

NSUserDefaults not writing to disk

I have a small background Mac (menubar) app which uses preferences for one simple value (string). When I launch the app from Xcode, it will almost always report the pref as being set, even when the plist does not exist in ~/Library/Preferences. Without making any codebase changes, it appears to occassionally write the prefs file upon subsequent launches. It does have a helper app which runs normally, and a Today extension, if it matters. I've tried using the [defaults synchronize] command, it doesn't do anything. I have other apps which work fine. I don't access or set NSUSerDefaults (including bindings) anywhere else in my project
Currently, my app is NOT sandboxed.
some sample code:
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
[standardDefaults synchronize];
NSLog(#"value for key is %#", [standardDefaults valueForKey:#"firstTime"]);
//this always reports back as "Accepted" even when no pref exists
if ([[standardDefaults valueForKey:#"first"] isEqualToString:#"Accepted"])
{
NSLog(#"accepted");
//always seen
}
else
{
//rarely seen except in complete random times
NSLog(#"first time");
#try
{
NSLog(#"alert");
[standardDefaults setObject:#"Accepted" forKey:#"first"];
[standardDefaults synchronize];
}
#catch (NSException *exception)
{
NSLog(#"exception is %#", exception);
}
#finally
{
}
}
UDPATE: I now see this on new apps, non-sandboxed. Bizarre. I created a brand-new mac/Cocoa app, and used this code:
- (void)viewDidLoad
{
[super viewDidLoad];
NSUserDefaults *standardDefaults = [NSUserDefaults standardUserDefaults];
[standardDefaults synchronize];
NSLog(#"value for key is %#", [standardDefaults valueForKey:#"firstTime"]);
if (![standardDefaults valueForKey:#"firstTime"])
{
NSLog(#"settings value for key");
[standardDefaults setValue:#"Whatever" forKey:#"firstTime"];
[standardDefaults synchronize];
}
NSLog(#"value for key is %#", [standardDefaults valueForKey:#"firstTime"]);
// Do any additional setup after loading the view.
}
The first time, it reports as missing. Every attempt afterwards, tt immediately registers as the pref existing and reporting as "whatever", even when the pref file is most definitely not there.
The file is normally periodically written by cfprefsd. All times you read/write the values you're actually talking to cfprefsd, not to the file on disk.
When you synchronize, it causes cfprefsd to get the values that you've set, again, not guaranteeing that it is written to disk.
The condition you've stated seems to indicate that the preference was set the first time round, and even though you've not seen the file appear on disk, it's reading the values. This is the expected behaviour.
If you want to test resetting the preferences before writing, you need to clear them out, using something like:
NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain];
I believe that since the introduction of cloud preference syncing, it's become a more noticeable issue that the file doesn't appear. Modifying the file has never been considered stable.
If you want to delete the preferences from the command line, you should use defaults delete (I don't have a mac at hand to give you the exact command line for this, though).

Check for first launch of my application

How would I check if it is the first launch of of my application using NSUserDefaults and running some code for the first time my app opens?
This should point you in the right direction:
static NSString* const hasRunAppOnceKey = #"hasRunAppOnceKey";
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:hasRunAppOnceKey] == NO)
{
// Some code you want to run on first use...
[defaults setBool:YES forKey:hasRunAppOnceKey];
}
The NSUserDefaults answer is the first thing that popped in my head, but upon reflection I will make another suggestion. A bit more work, but it's worth considering. The motive is: sometimes when troubleshooting an app, Apple recommends deleting that app's plist file. It's a fairly ubiquitous troubleshooting technique. I would recommend storing your boolean in your plist file instead of NSUserDefaults.
Disclaimer: I only do iOS development, so I'm not sure how NSUserDefaults and plists interact on the Mac, and I don't know what all is involved in getting your plist to live in ~/Library/Application\ Support/Preferences/com.mycompany.MyAppName.plist
Anyway, I imagine what this requires is having some code which can actually author a "fresh" plist (probably a copy from a template file in your bundle), and you app does this if it launches and does not see a plist. The default plist should not include the flag which lets your users skip the 'first time' code, but if they have opened the app before, and then delete the plist, they should get default behavior back.
This is an important behavior to support where possible, to aide our users if our app ever gives them trouble.
if (![[NSUserDefaults standardUserDefaults] boolForKey:#"hasBeenLaunched"]) {
// Run code on the first launch only ...
[defaults setBool:YES forKey:#"hasBeenLaunched"];
}
You can use NSUserDefaults to save bools, integers, objects into the program and have them available whenever you open it. You can use 'boolForKey' to set a flag called "hasBeenLaunched". By default, this value will be NO when not set. Once you change it to YES, the code in the if condition will never be executed again.
In your main controller class, implement something like this:
static NSString * const MDFirstRunKey = #"MDFirstRun";
#implementation MDAppController
+ (void)initialize {
NSMutableDictionary *defaults = [NSMutableDictionary dictionary];
[defaults setObject:[NSNumber numberWithBool:YES] forKey:MDFirstRunKey];
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
// the following if on Mac and is necessary:
[[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:defaults];
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
BOOL firstRun = [[[NSUserDefaults standardUserDefaults]
objectForKey:MDFirstRunKey] boolValue];
if (firstRun) {
// do something
[[NSUserDefaults standardUserDefaults] setObject:
[NSNumber numberWithBool:NO] forKey:MDFirstRunKey];
} else {
// do something else
}
}
#end
The +initialize class method is called before an instance of the class it's found in is created; in other words, it is called very early on, and is a good place to set up your default values.
See Preferences and Settings Programming Guide: Registering Your App's Default Preferences for more info.

Access iTunes user preferences in OSX Lion

I want to access iTunes user preferences such as playlists programmatically.
I use to do it with the following code, however since OSX Lion, I get a nil in response.
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSDictionary *userPreferences = [userDefaults persistentDomainForName:#"com.apple.iApps"];
NSArray *databasePaths = [userPreferences objectForKey:#"iTunesRecentDatabasePaths"];
I've also made sure my app has all of its entitlements enabled.
Any suggestions on how I can fix this?
Long story made short: You just can not do it using a Sandboxed app. Turn off Sandboxing and you will see that it works. Why? Well, that's because of containers. A sandbox lives in its own container and so when you call [NSUserDefaults standardUserDefaults] Cocoa uses the path of your container rather than the POSIX path of ~/Library/Preferences which is where com.apple.iApps.plist resides. That sums up why you get nil. Also, there is a blurb on this here in NSUserDefaults: link
How to fix this?
It's really not to bad. First you have to do a little bit of work to get the POSIX path of your home directory. I setup a bunch of NSURL category methods. However the root path is POSIX based. Here is the code snippet to get you started.
1.
+ (NSURL *) homePOSIXURL {
struct passwd *pwUser = getpwuid(getuid());
const char *homeDir = pwUser->pw_dir;
return [NSURL fileURLWithPath:[NSString stringWithUTF8String:homeDir] isDirectory:YES]; }
When all is said and done, construct a full path to the plist. It hasn't changed for years so you can consider it sticky.
So you might get something that looks like this now:
2.
NSURL * prefURL = [[NSURL libraryPOSIXURL] URLByAppendingPathComponent:#"Preferences" isDirectory:YES];
prefURL = [prefURL URLByAppendingPathComponent:#"com.apple.iApps.plist"];
Now let's turn this plist, which is fairly small into something we can play with. Perhaps NSData? That sounds good, right?
3.
NSData * prefData = [NSData dataWithContentsOfURL:prefURL];
Ok, finally we can use this now to get an NSDictionary. Here is how I do it.
4.
NSDictionary * prefDict = [NSDictionary collectionFromPropertyList:prefData];
Yeah, yeah another Category on NSDictionary. I must have a million of them.
Because I'm in a sharing mood, here ya go:
+ (id) collectionFromPropertyList:(NSData *)pList {
if ( [pList length] > 0 )
return [NSPropertyListSerialization propertyListWithData:pList
options:NSPropertyListImmutable
format:nil error:nil];
return nil;
}
So, you think we are done? Well, almost. If you get this far, you will get a deny like so:
deny file-read-data
/Users/UserName/Library/Preferences/com.apple.iApps.plist
Are we loving our Sandboxed app?! Basically add your temporary entitlement and you should be off to the races again. Best of luck to ya!

How to know if window is minimizable when titlebar was double clicked?

This image is from SystemPreferences > Appearance
I want to know How do I get that value programmatically?
I ask because I am drawing a window with a customized titlebar and I want it to resemble (in behavior) as much as possible to normal (non-customized) cocoa windows.
Maybe a terminal command I can pipe or is there an cocoa API that does this?
EDIT:
Answer (thanks to NSGod)
- (void)mouseUp:(NSEvent *)event{
if ([event clickCount] == 2) {
//Get settings from "System Preferences" > "Appearance" > "Double-click on windows title bar to minimize"
NSString *const MDAppleMiniaturizeOnDoubleClickKey = #"AppleMiniaturizeOnDoubleClick";
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
// [userDefaults addSuiteNamed:NSGlobalDomain]; // unnecessary
BOOL shouldMiniaturize = [[userDefaults objectForKey:MDAppleMiniaturizeOnDoubleClickKey] boolValue];
if (shouldMiniaturize) {
[self miniaturize:self];
}
}
}
Later I found that Appearance (Aqua/Graphite) can be found:
NSString * const kAppleAquaColorVariant = #"AppleAquaColorVariant";
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
// [userDefaults addSuiteNamed:NSGlobalDomain]; // unnecessary
NSNumber *color = [userDefaults objectForKey:kAppleAquaColorVariant];
if ([color intValue] == 6) {//graphite is 6
imageName = [imageName stringByAppendingFormat:#"_graphite"];
}else{//defaults to aqua, (aqua is 1)
imageName = [imageName stringByAppendingFormat:#"_colorsryg"];
}
Which can be helpful too :)
The way I would do it is probably read the value in from user defaults.
NSString * const MDAppleMiniaturizeOnDoubleClickKey = #"AppleMiniaturizeOnDoubleClick";
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
// [userDefaults addSuiteNamed:NSGlobalDomain]; // unnecessary
NSNumber *miniaturize = [userDefaults
objectForKey:MDAppleMiniaturizeOnDoubleClickKey];
NSLog(#"AppleMiniaturizeOnDoubleClick == %#",
([miniaturize boolValue] ? #"YES" : #"NO"));
(This preference setting is stored in the invisible .GlobalPreferences.plist in your ~/Library/Preferences/ folder).
Note that by default, the "double-click to minimize" option is turned off, so if you check for the presence of the AppleMiniaturizeOnDoubleClick and it returns nil, that means it is off. (User defaults only start to store values if they differ from the defaults).
This key is the same in Leopard as it is in Snow Leopard. (It hasn't changed in Lion or Mountain Lion either).
Of course, there is a secret (private) method in NSWindow, -(BOOL)_shouldMiniaturizeOnDoubleClick, but I wouldn't recommend using private methods.
[UPDATE] Regarding Catfish_Man's comment: you are correct in that the line [userDefaults addSuiteNamed:NSGlobalDomain]; is unnecessary, as NSUserDefaults already has the ability to read global preferences. (I modified the code above to reflect this).
"Additionally, NSGlobalDomain is not translated to
.GlobalPreferences.plist for that method."
I'm not sure I follow you there. NSUserDefaults is built on top of CFPreferences which defines the following 6 constants:
Application:
kCFPreferencesAnyApplication,
kCFPreferencesCurrentApplication
Host:
kCFPreferencesAnyHost,
kCFPreferencesCurrentHost
User:
kCFPreferencesAnyUser,
kCFPreferencesCurrentUser
Given a fictional application bundle identifier of "com.markdouma.App" and a single host (based on your current network location that won't change for this example), there are generally 8 locations where preference information could be stored on your disk. (NOTE: The paths shown are for demonstration purposes only; the exact file path locations are subject to change). The 8 different locations arise from the different combination of the CFPreferences constants:
/Library/Preferences/.GlobalPreferences.plist
(kCFPreferencesAnyApplication, kCFPreferencesAnyUser, kCFPreferencesAnyHost)
/Library/Preferences/com.markdouma.App.plist
(kCFPreferencesCurrentApplication, kCFPreferencesAnyUser,
kCFPreferencesAnyHost)
/Library/Preferences/ByHost/.GlobalPreferences.UNIQUE_HOST_IDENTIFIER.plist (kCFPreferencesAnyApplication, kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
/Library/Preferences/ByHost/com.markdouma.App.UNIQUE_HOST_IDENTIFIER.plist (kCFPreferencesCurrentApplication,
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
~/Library/Preferences/.GlobalPreferences.plist
(kCFPreferencesAnyApplication, kCFPreferencesCurrentUser,
kCFPreferencesAnyHost)
~/Library/Preferences/com.markdouma.App.plist
(kCFPreferencesCurrentApplication, kCFPreferencesCurrentUser,
kCFPreferencesAnyHost)
~/Library/Preferences/ByHost/.GlobalPreferences.UNIQUE_HOST_IDENTIFIER.plist (kCFPreferencesAnyApplication,
kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
~/Library/Preferences/ByHost/com.markdouma.App.UNIQUE_HOST_IDENTIFIER.plist (kCFPreferencesCurrentApplication,
kCFPreferencesCurrentUser, kCFPreferencesCurrentHost)
While NSUserDefaults can only write to the domain combination shown in italics, it automatically has read access to the domain combinations shown in bold. In other words, without having to do anything, I can automatically run the following code and print the results:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSNumber *miniaturize = [userDefaults
objectForKey:#"AppleMiniaturizeOnDoubleClick"];
NSNumber *fastUserSwitching = [userDefaults
objectForKey:#"MultipleSessionEnabled"];
NSLog(#"AppleMiniaturizeOnDoubleClick == %#",
([miniaturize boolValue] ? #"YES" : #"NO"));
NSLog(#"MultipleSessionEnabled == %#",
([fastUserSwitching boolValue] ? #"YES" : #"NO"));
Running that code on my system prints the following results:
AppleMiniaturizeOnDoubleClick == YES
MultipleSessionEnabled == YES
This makes sense, since I have both Fast User Switching and Double-click to minimize options enabled. MultipleSessionsEnabled is stored in the local domain at /Library/Preferences/.GlobalPreferences.plist, and AppleMiniaturizeOnDoubleClick is stored in the user domain at ~/Library/Preferences/.GlobalPreferences.plist.
Sample project: NSUserDefaultsFinagler.zip
"Additionally additionally, that's slow. Please don't do this."
Sorry, but that makes no sense (assuming that we've agreed that we're no longer using addSuiteNamed:). User defaults are cached by the application, making calls take in the matter of milliseconds. Wouldn't there be little noticeable difference between asking user defaults for the value for a key that represents a local application value or one that represents a global value?
AFAIK, this is the only "legal" (App-store-compatible) way to achieve the OP's goal. If there's another more efficient means, then please elaborate.