Is it possible to use bindings in the nib with ScreenSaverDefaults? - macos

I'm working on a screensaver, so I'm supposed to use ScreenSaverDefaults instead of NSUserDefaults. I'd like to have my configure panel use bindings for its UI, but they need to be wired to ScreenSaverDefaults, and I can't see a way to do that; the only defaults controller available in IB (xCode) is the standard user defaults controller. Is there a workaround, or is it just not possible to use bindings in the nib in the context of a screensaver?

I have successfully used bindings in a screen saver.
(Possibly important note: I'm still developing on 10.6, with XC 3.2.6; this code has only been lightly tested so far on 10.10 (but does seem to work). Also, I mix C++ and Objective-C, so you may have minor code cleanup to do, in what follows.)
My code creates, and the UI binds to, a custom user defaults controller (controls bind to File's Owner, which is set to my screen saver view, the controller key is empty, and the model key path is self.defaultsController.values.key_val, where key_val is whatever key is used in the defaults plist to access the value bound to a control).
You must also create your own screen saver defaults, and direct your custom user defaults controller to use them, early in screen saver initialization (such as in the initWithFrame: method of your screen saver view), like so:
// Find our bundle:
NSBundle * screenSaverBundle = [NSBundle bundleForClass: [self class]];
// Load per-user screen saver defaults:
ScreenSaverDefaults * screenSaverDefaults = [ScreenSaverDefaults defaultsForModuleWithName: [screenSaverBundle bundleIdentifier]];
// Create default defaults (values to use when no other values have been established):
NSDictionary * defaultDefaultValues = [self defaultDefaults];
// Register default defaults as fallback values (in registration domain):
[screenSaverDefaults registerDefaults: defaultDefaultValues];
// Configure custom user defaults controller to use the correct defaults:
// NOTE: defaultsController of NSUserDefaultsController * type *MUST* be declared using #property in your header, and accessors must be provided (typically using #synthesize in the implementation section).
defaultsController = [[NSUserDefaultsController alloc] initWithDefaults: screenSaverDefaults initialValues: nil]; // <- change nil to factory defaults, if desired
// Make sure changes are only saved when committed by user:
[defaultsController setAppliesImmediately: false];
(The above code was slightly rearranged from various methods in my own code; might have a typo or two, but the gist is correct.)
An implementation of defaultDefaults looks something like this (pardon my unconventional style):
- (NSDictionary *) defaultDefaults
{
NSString * ResourcesPath = [[self screenSaverBundle] resourcePath];
NSString * DefaultDefaultsPath = [ResourcesPath stringByAppendingPathComponent: #"DefaultDefaults.plist"];
NSDictionary * DefaultDefaults = [NSDictionary dictionaryWithContentsOfFile: DefaultDefaultsPath];
return DefaultDefaults;
}
If you want to provide a "factory reset" capability in your screen saver, you'll need to set the initialValues: argument to an "original factory values" dictionary when creating the custom user defaults controller. In response to a "Reset to Factory Defaults" button, simply send revertToInitialValues: to the controller.
Finally, please note that depending on the intended lifetime of some of the objects created in the above code, you may need to retain some of them (and release them properly later). I assume you understand this bhaller; I'm just pointing this out for the general audience.

Swift Version:
First, register default datas.
let defaults = ScreenSaverDefaults.init(forModuleWithName: "com.your.xxxx" ?? "")!
defaults.register(defaults: ["isShowSecond":true,
"userThemeIndex":0,
"userClockIndex":0
])
You can set value:
defaults.set(false, forKey: "isShowSecond")
You can get value:
let defaults = ScreenSaverDefaults.init(forModuleWithName: "com.your.xxxx" ?? "")!
let userClockIndex = defaults.bool(forKey: "isShowSecond")

Related

Separate NSPopUpButton content from label while using bindings

I have an NSPopupButton whose content is bound to an NSArray, let’s say the array is
#[
#"Option 1",
#"Option 2"
];
Its selected object is bound to User Defaults Controller, and is written to a preference file by the user defaults system.
In my code I check whether the preference is set to #"Option 1" or not, and perform actions accordingly.
This all worked well (though I did feel a little uneasy checking for what is essentially a UI value, but whatever...) until I needed to localize.
Because the value is the label, I’m having an issue.
If my user is in France, his preferences file will say #"L’option 1", which is not equal to #"Option 1". I need to abstract the presentation from the meaning and it's proving pretty difficult.
I split up the binding into two arrays, let's call them values and labels.
Let’s say they look like this:
values = #[
#"option_1",
#"option_2"
];
labels = #[
NSLocalizedString(#"Option 1", nil),
NSLocalizedString(#"Option 2", nil)
];
I’ve bound the NSPopUpButton’s Content binding to values and its Content Values binding to labels. However, the popup list is showing option_1 and option_2, it does not seem to want to use the labels array to label the items in the popup button.
How do I get the NSPopUpButton to use values internally and store that in the preferences file, but display labels to the user?
It doesn’t have to be architected this way, if you can think of a better solution. The point is I want to store and check one value, and have that value associated with a label that gets localized appropriately.
Cocoa bindings work very well with value transformers, because you can apply them directly in the bindings window, for example
#implementation LocalizeTransformer
+ (Class)transformedValueClass
{
return [NSArray class];
}
+ (BOOL)allowsReverseTransformation
{
return NO;
}
- (id)transformedValue:(id)value {
if (![value isKindOfClass:[NSArray class]]) return nil;
NSMutableArray *output = [NSMutableArray arrayWithCapacity:[value count]];
for (NSString *string in value) {
[output addObject:NSLocalizedString(string, nil)];
}
return [output copy];
}
#end
you have to register the transformer in awakeFromNib or better in +initialize
NSValueTransformer *localizeTransformer = [[LocalizeTransformer alloc] init];
[NSValueTransformer setValueTransformer:localizeTransformer
forName:#"LocalizeTransformer"];
then it appears in the popup menu of value transformers
Bind Selected Tag to your User Defaults Controller instead of Selected Object.
If the NSPopupButton choices are fixed add the NSMenuItems in Interface Builder and set their Tags. Otherwise bind an array of NSMenuItem, again with proper Tags.
Selected Index would also work but only until you change the order.

Drag and drop files into NSOutlineView

I'm trying to implement simple drag and drop operation into NSOutlineView Based on Apple's example - https://developer.apple.com/library/mac/samplecode/SourceView/Introduction/Intro.html
All seems to be ok, but finally when I drop some files from Finder I get error:
[<ChildNode 0x60800005a280> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key description.') was raised during a dragging session
Here is my test project: https://www.dropbox.com/s/1mgcg2dysvs292u/SimpleDrag.zip?dl=0
What I exactly need in my app: allow user to drag and drop multiple files and folder into some tree list and then display them to user. Also save all this this into some file, so it can be loaded again with all user dragged files and folders.
A final result I want to have like this:
The description property of NSObject is read-only, and is generally set by providing a getter in the implementation file:
- (NSString *)description {
return [self urlString]; // Using urlString solely for demo purposes.
}
You can't set it, either via key-value coding or by direct assignment:
self.description = [self urlString]; // Xcode error: 'Assignment to readonly property'
[self setValue:[self urlString] forKey:#"description"];
In -[ChildNode copyWithZone:] an attempt is made to do the latter of the two, and that's what causes the warning to be logged to the console.
// -------------------------------------------------------------------------------
// copyWithZone:zone
// -------------------------------------------------------------------------------
- (id)copyWithZone:(NSZone *)zone
{
id newNode = [[[self class] allocWithZone:zone] init];
// One of the keys in mutableKeys is 'description'...
// ...but it's readonly! (it's defined in the NSObject protocol)
for (NSString *key in [self mutableKeys])
{
[newNode setValue:[self valueForKey:key] forKey:key];
}
return newNode;
}
This begs the question why do you get the warning in your app, and not in the sample app? From what I can tell no ChildNode instance is ever sent a copyWithZone: message in the sample app, whereas this does happen in your app, immediately after the drop. Of course there's a second question here as well: why do Apple explicitly include the description key-path when it can't be set this way? - unfortunately I can't help you with that.
A really handy way of trying to trap errors that don't actually cause exceptions is to add an All Exceptions breakpoint. If you do this in your sample app you'll see that the app freezes at the line that's causing the problem, giving you a better chance of figuring out the issue.

NSButton setAction selector

I just want to add a NSButton with setAction Arguments.
NSRect frame = NSMakeRect(10, 40, 90, 40);
NSButton* pushButton = [[NSButton alloc] initWithFrame: frame];
[pushButton setTarget:self];
[pushButton setAction:#selector(myAction:)];
But I want to put an argument to the function myAction...
How ?
Thanks.
But I want to put an argument to the function myAction...
How ?
You can't.
… if there is more than one button that uses this method, we can not differentiate the sender (only with title)...
There are three ways to tell which button (or other control) is talking to you:
Assign each button (or other control) a tag, and compare the tags in your action method. When you create controls in a nib, this has the downside that you have to write the tag twice (once in the code, once in the nib). Since you're writing out the button by hand from scratch, you don't have that problem.
Have an outlet to every control that you expect to send you this message, and compare the sender to each outlet.
Have different action methods, with each control being the only one wired up to each action. Each action method then does not need to determine which control sent you that message, because you already know that by which method it is.
The problem with tags is the aforementioned repetitiveness. It's also very easy to neglect to name each tag, so you end up looking at code like if ([sender tag] == 42) and not knowing/having to look up which control is #42.
The problem with outlets is that your action method may get very long, and anyway is probably doing multiple different things that have no business being in the same method. (Which is also a problem with tags.)
So, I generally prefer the third solution. Create an action method for every button (or other control) that will have you as its target. You'll typically name the method and the button the same (like save: and “Save”) or something very similar (like terminate: and “Quit”), so you'll know just by reading each method which button it belongs to.
I never programatically created an NSButton, but I think that you just need to create a method like this:
- (void) myAction: (NSButton*)button{
//your code
}
And that's it !!
You can use associated Objects for passing arguments.
You can refer : http://labs.vectorform.com/2011/07/objective-c-associated-objects/
http://www.cocoanetics.com/2012/06/associated-objects/
.tag should be sufficient if your object have any integer uniqueID.
I use .identifier instead, since it support string based uniqueID.
Example:
...
for (index, app) in apps.enumerated() {
let appButton = NSButton(title: app.title, target: self, action: #selector(appButtonPressed))
appButton.identifier = NSUserInterfaceItemIdentifier(rawValue: app.guid)
}
...
#objc func appButtonPressed(sender: NSButton) {
print(sender.identifier?.rawValue)
}

Overriding "Edited" in window title for NSDocument

How do I prevent a window title from displaying "Edited" for an NSDocument which is dirty?
I'm managing saving and autosaving myself, using a web service, and just don't want the distraction in the title bar.
I've tried overriding:
NSDocument's -isDocumentEdited and -hasUnautosavedChanges always to return NO.
-[NSWindowController setDocumentEdited] to do nothing, or always to use NO regardless of the parameter's actual value.
-[NSWindowController synchronizeWindowTitleWithDocumentName] to do nothing.
-[NSWindow setDocumentEdited] to do nothing, or always to use NO regardless of the parameter's actual value.
In all cases, the title bar still changes to Edited when I make changes to a saved document.
If I override -[NSDocument updateChangeCount:] and -[NSDocument updateChangeCountWithToken:forSaveOperation:] to do nothing, I can prevent this from happening, but it affects saving, autosaving, and other document behaviors, too.
I also tried this:
[[self.window standardWindowButton: NSWindowDocumentVersionsButton] setTitle:nil];
That displayed a blank string instead of Edited, but the dash still appeared – the one which normally separates the document name and Edited.
Any idea how to pry apart this part of the window from the document?
Several options:
To get a pointer to the "dash", look for a TextField in [window.contentView.superview.subviews] with a stringValue equals to "-". You can set its text to an empty string as well.
#implementation NSWindow (DashRetrivalMethod)
- (NSTextField*)versionsDashTextField
{
NSTextField* res = nil;
NSView* themeFrame = [self.contentView superview];
for (NSView* tmp in [themeFrame subviews])
{
if ([tmp isKindOfClass:[NSTextField class]])
{
if ([[(NSTextField*)tmp stringValue] isEqualToString:#"—"])
{
res = (NSTextField*)tmp;
break;
}
}
}
return res;
}
#end
You can override NSWindow's -setRepresentedURL:. This would also affect the NSWindowDocumentIconButton and the popup menu, but you can manually create it if you want by: [NSWindow standardWindowButton: NSWindowDocumentIconButton].
Override one of these three NSDocument's undocumented methods:
// Always return here NO if you don't want the version button to appear.
// This seems to be the cleanest options, besides the fact that you are
/// overriding a private method.
- (BOOL)_shouldShowAutosaveButtonForWindow:(NSWindow*)window;
// Call super with NO
- (void)_setShowAutosaveButton:(BOOL)flag;
// Here the button and the dash are actually created
- (void)_endVersionsButtonUpdates;
// Here Cocoa hide or unhide the edited button
- (void)_updateDocumentEditedAndAnimate:(BOOL)flag
Have you tried overriding NSDocuments - (BOOL)hasUnautosavedChanges in addition to overriding - (BOOL) isDocumentEdited?
Although this is a late answer, you can easily determine what is going to be the title of your NSDocument window by overriding
- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName
in your NSWindowController and return the appropriate title.
You can do that also by overriding the property of your NSDocument:
- (NSString *)displayName
but this is not recommended by Apple, because that is normally used by the OS error handlers.
I added this answer, because none of the other answers really set me on the right path.

How to test with NSUserDefaults?

In most of my classes that work with defaults I make the defaults object settable:
#property(retain) NSUserDefaults *defaults;
This is supposed to make testing easier:
// In a nearby test class:
- (void) setUp {
[super setUp];
NSUserDefaults *isolatedDefaults = [[NSUserDefaults alloc] init];
[someObjectBeingTested setDefaults:isolatedDefaults];
}
But now I have found out then when I create a fresh defaults object, there are already some values in it. Is that possible? I thought I could create an empty, isolated defaults object by calling -init. Do I have a bug somewhere in the testing code, or do I really have to do something more complex (like stubbing or mocking) if I want to test my defaults-based code?
In the end I have created a simple NSUserDefaults replacement that can be used to control the defaults environment in tests. The code is available on GitHub.
From the NSUserDefaults documentation:
init: Returns an NSUserDefaults object initialized with the defaults for the current user account.
This should normally not be empty. I am not really sure what you want to test here, since it would be a waste of time to test NSUserDefaults functionality.
But say you need some keys to be not registered yet for your test to always have the same initial point: then just remove them in setUp (and restore them later in tearDown if you want to).
Something like:
- (void) setUp {
[super setUp];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"myTestKey"];
// synchronize the change, or just use resetStandardUserDefaults:
[someObjectBeingTested setDefaults:[NSUserDefaults resetStandardUserDefaults]];
}
If you don't have a specific list of keys but need to wipe out everything, you will have to use the CoreFoundation Preferences Utilities, see CFPreferencesCopyKeyList.
We also needed to override NSUserDefaults for testing, but didn't want to change any of the application code.
So we wrote a category on NSUserDefaults that allows us to override values returned by objectForKey: at runtime using method swizzling.
It looks like this in Objective C:
NSLog(#"Value before: %d", [[NSUserDefaults standardUserDefaults] boolForKey:#"Example"]);
// Value before: 0
[[NSUserDefaults standardUserDefaults] overrideValue:#(YES) forKey:#"Example"];
NSLog(#"Value after: %d", [[NSUserDefaults standardUserDefaults] boolForKey:#"Example"]);
// Value after: 1
And like this in Swift:
print(UserDefaults.standard.bool(forKey: "ExampleKey")) // false
UserDefaults.standard.overrideValue(true, forKey: "ExampleKey")
print(UserDefaults.standard.bool(forKey: "ExampleKey")) // true
You can find our code on Github: https://github.com/jakob/NSUserDefaultsOverride

Resources