I need to set the property Enabled of a control in interface builder, depending on 2 booleans int he preferences.
However the operator should be OR and not AND. If one of the two is true, than my control should be enabled.
Currently, I can only make it work with an AND operator, (See screenshot).
Thanks
Unfortunately, in IB, you're stuck with and. My suggestion would be to add a new property to an object accessible to your NIB (possibly your owner for the NIB), which is dependent on changes to the other objects in order to enable your control/view.
It looks like you're using the Shared User Defaults Controller, so I would suggest that in the owner you create a new boolean property for your combined user defaults (perhaps downloadingCastOrCrew), and then you'll need to make sure that when either of the defaults change, you change the value of downloadingCastOrCrew:
In your Interface:
#property BOOL downloadingCastOrCrew;
In the implementation as you're setting up your controller or after awakeFromNib:
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self
forKeyPath: #"values.kSearchPreferencesDownloadCast"
options:NSKeyValueObservingOptionNew
context:NULL];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self
forKeyPath: #"values.kSearchPreferencesDownloadCrew"
options:NSKeyValueObservingOptionNew
context:NULL];
In the implementation as you're tearing down your controller:
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver: self
forKeyPath: #"values.kSearchPreferencesDownloadCast"];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver: self
forKeyPath: #"values.kSearchPreferencesDownloadCrew"];
Add an observer if you don't already have one:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
self.downloadingCastOrCrew = [defaults boolForKey: #"kSearchPreferencesDownloadCast"]
|| [defaults boolForKey: #"kSearchPreferencesDownloadCrew"];
}
By using the accessor method, you'll trigger kvo and you'll be able to use the controller's downloadingCastOrCrew as your boolean to check instead of the NSUserDefaults values directly.
Obviously, if you already have an observeValueForKeyPath, you will likely want to add a context value to the addObserver:forKeyPath:options:context call and check it in the observeValueForKeyPath:ofObject:change:context call.
You can do it in this way :
Create a third property
#property BOOL isFirst;
#property BOOL isSecond;
#property BOOL isTextFieldVisible;//this one is your third
- (IBAction)isSec:(id)sender;
- (IBAction)isFir:(id)sender;
In implementation
- (id)init
{
self = [super init];
if (self) {
self.isFirst=NO;
self.isSecond=NO;
}
return self;
}
- (IBAction)isSec:(id)sender {
self.isSecond=!self.isSecond;
[sender setTitle:[NSString stringWithFormat:#"isSecond: %d",self.isSecond]];
self.isTextFieldVisible=self.isFirst || self.isSecond;
self.isTextFieldVisible=!self.isTextFieldVisible;
NSLog(#"->%d",self.isTextFieldVisible);
}
- (IBAction)isFir:(id)sender {
self.isFirst=!self.isFirst;
[sender setTitle:[NSString stringWithFormat:#"isfirst: %d",self.isFirst]];
self.isTextFieldVisible=self.isFirst || self.isSecond;
self.isTextFieldVisible=!self.isTextFieldVisible;
NSLog(#"->%d",self.isTextFieldVisible);
}
#end
And in the binding just bind the textField to third property,
Check the running application here.
EDIT 1:
Change ValueTransformer NSNegateBoolean in IB. so that my two lines self.isTextFieldVisible=!self.isTextFieldVisible; is not required in both the IBAction.
Related
I am just experimenting with disabling a button with various methods on XCode MacOS (not iOS) Cocoa Objective-C.
In this scenario I have a help button (m_btHelp) that is disabled when g_bEnableHelpButton = NO; but it is only being checked when the mouse moves.
-(void)mouseMoved:(NSEvent *)theEvent
{
if(g_bEnableHelpButton) {
[m_btHelp setEnabled:YES];
} else {
[m_btHelp setEnabled:NO];
}
I would rather have this continuously checked instead of only checked when the mouse moves. I have tried NSTimer with something like this, but it does not seem to work (m_btHelp does not get disabled when g_bEnableHelpButton = NO; like it does in the mouseMoved event:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(Timerloop) userInfo:nil repeats:YES];
}
- (void)Timerloop
{
if(g_bEnableHelpButton) {
[m_btHelp setEnabled:YES];
} else {
[m_btHelp setEnabled:NO];
}
}
g_bEnableHelpButton is a global variable, right? Don't use global variables. It's way better to create a class which holds your state (can be view model, ...). I'm going to skip a state class in all examples below and will use BOOL helpButtonEnabled property on the same view controller (this is not mandatory, it just makes all these examples a bit shorter). You can move this property elsewhere, it can be a state class, it can be basically any object.
Another thing is this NSTimer, NSTrackingArea, ... With all these things one is wasting CPU cycles, battery life, ... Cocoa & Objective-C offers various ways how to monitor a property value and react to it. You can override property setter, you can use KVO or bindings. All three methods are covered in examples below.
There are other ways for sure (like ReactiveCocoa), but I'd like to demonstrate three ways how to achieve it without dependencies.
Initial state
Imagine you have this view:
With the following implementation:
#import "ViewController.h"
#interface ViewController ()
// Help button from the Main.storyboard
// Imagine it's your m_btHelp
#property (nonatomic, strong) IBOutlet NSButton *helpButton;
// Property driving helpButton enabled/disabled state
// Imagine it's your g_bEnableHelpButton
#property (nonatomic, getter=isHelpButtonEnabled) BOOL helpButtonEnabled;
#end
#implementation ViewController
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
There's help button and there's toggle button which just toggles helpButtonEnabled value (YES -> NO, NO -> YES).
How to monitor it without timer, tracking area, ... to update the help button state?
Override setter
Encapsulating Data.
#implementation ViewController
// This is setter for the helpButtonEnabled property.
- (void)setHelpButtonEnabled:(BOOL)helpButtonEnabled {
// If the new value equals, do nothing
if (helpButtonEnabled == _helpButtonEnabled) {
return;
}
// Update instance variable
_helpButtonEnabled = helpButtonEnabled;
// Update button state
_helpButton.enabled = helpButtonEnabled;
}
- (void)viewDidLoad {
[super viewDidLoad];
// When the view loads update button state to the initial value
_helpButton.enabled = _helpButtonEnabled;
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
KVO
Introduction to Key-Value Observing Programming Guide.
static void * const ViewControllerHelpButtonEnabledContext = (void*)&ViewControllerHelpButtonEnabledContext;
#implementation ViewController
- (void)dealloc {
// Remove previously registered observer when the view controller goes away
[self removeObserver:self forKeyPath:#"helpButtonEnabled" context:ViewControllerHelpButtonEnabledContext];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Register observer for the helpButtonEnabled key path
// - it fires immeditately with the current value (NSKeyValueObservingOptionInitial)
// - it fires later every single time new value is assigned (NSKeyValueObservingOptionNew)
// - context is used to quickly distinguish why the observeValueForKeyPath:... was called
[self addObserver:self
forKeyPath:#"helpButtonEnabled"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:ViewControllerHelpButtonEnabledContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == ViewControllerHelpButtonEnabledContext) {
// It's our observer, let's update button state
_helpButton.enabled = _helpButtonEnabled;
} else {
// It's not our observer, just forward it to super implementation
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
Binding
Introduction to Cocoa Bindings Programming Topics.
#implementation ViewController
- (void)dealloc {
// Remove binding when the view controller goes away
[self.helpButton unbind:NSEnabledBinding];
}
- (void)viewDidLoad {
[super viewDidLoad];
// self.helpButton.enabled is binded to self.helpButtonEnabled
[self.helpButton bind:NSEnabledBinding
toObject:self
withKeyPath:#"helpButtonEnabled"
options:nil];
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
in a slide menu I'm developing for my project i would like to add a black view over the content view when it's slide out. To do this i need to create a method that check continuously the view x-position and darken or brighten up the black layer. The position of this view is the same as the content view.
I thought i can use a NSNotificationCenter like this:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(incomingNotification:) name:#"notification" object: darkViewController.view.frame.origin.x]];
and a method:
- (void) incomingNotification:(NSNotification *)notification{
// the dark layer alpha will be 0 at origin=0 and 0.8 at bounds.size.width
float alphaToUse = (darkViewController.view.frame.origin.x / self.view.bounds.size.width) * 0.8;
[darkViewController.view setAlpha:alphaToUse];
}
The problem is that i must use an object as parameter.
I'm new to notifications so i'm asking: is it wrong to use them for this kind of things?
Is it better to solve this in another way?
EDIT:
Following Denis advice i'm now trying to use the key-value-observe solution.
My app is structured like this:
MenuViewController-->ContainerViewController-->DarkViewController
In MenuViewController.m :
#interface MenuViewController ()
#property (strong,nonatomic) ContainerViewController *containerViewController;
#property (strong,nonatomic) DarkViewController *darkViewController;
#end
#implementation MenuViewController
#synthesize containerViewController,darkViewController;
# pragma mark - Views
- (void)viewDidLoad{
[super viewDidLoad];
containerViewController = [[ContainerViewController alloc]init];
[self addChildViewController:containerViewController];
[self.view addSubview:containerViewController.view];
[containerViewController didMoveToParentViewController:self];
darkViewController = [[DarkViewController alloc]init];
[containerViewController addChildViewController:darkViewController];
[containerViewController.view addSubview:darkViewController.view];
[darkViewController didMoveToParentViewController:containerViewController];
[UIView animateWithDuration:slideDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[darkViewController.view setAlpha:0.7];
containerViewController.view.frame = CGRectMake(self.view.frame.size.width - slideWidth, 0, self.view.frame.size.width, self.view.frame.size.height);
}
completion:^(BOOL finished) {
if (finished) {
}
}];
[darkViewController addObserver:self forKeyPath:#"darkViewController.view.frame.origin.x" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: (NSDictionary *)change context:(void *)context
{
NSLog(#"x is changed");
}
When i run this i get this exception:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<DarkViewController 0x10962d280> addObserver:<MenuViewController 0x10922c890> forKeyPath:#"darkViewController.view.frame.origin.x" options:1 context:0x0] was sent to an object that is not KVC-compliant for the "darkViewController" property.'
Ok, it seems that i found a solution following this example Notificationsin IOS
I just added this in the viewDidLoad of my ContainerViewController
[self addObserver:self forKeyPath:#"view.frame" options:0 context:nil];
and implemented the observer method with a for cycle to find my DarkViewController view
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
for (UIViewController * vc in self.childViewControllers) {
if ([vc isKindOfClass:[DarkViewController class]]) {
float alphaToUse = (self.view.frame.origin.x / self.view.bounds.size.width) * 0.8;
[vc.view setAlpha:alphaToUse];
}
}
}
Now i just have to understand where to put the removeObserver method, since my ContainerViewController will be always loaded...
There is another machanism in iOS for such kind of things called Key value coding and Key value observing.
From Notification Center documentation:
As you design your application, do not simply assume that you should send a notification to communicate with interested parties. You should also consider alternatives such as key-value observing, key-value binding, and delegation.
Key-value binding and key-value observing were introduced in OS X version 10.3 and provide a way of loosely coupling data. With key-value observing, you can request to be notified when the properties of another object change. Unlike regular notifications, there is no performance penalty for unobserved changes. There is also no need for the observed object to post a notification because the key-value observing system can do it for you automatically, although you can still choose do it manually.
So if you'll have another notification observers while making slide menu animation it may reduce its handling performance.
And the best solution would be to invoke incomingNotification: method inside the animation block (the method where animation performs).
Apple docs again:
Though key-value coding is efficient, it adds a level of indirection that is slightly slower than direct method invocations. You should use key-value coding only when you can benefit from the flexibility that it provides.
ANSWERING EDITED QUESTION:
This answer describes exactly what you're trying to do. When add the observer on some object's property object's name shouldn't be included in the property key path. So in you case adding an observer looks like this:
[darkViewController addObserver:self forKeyPath:#"view.frame" options:NSKeyValueObservingOptionNew context:nil];
When trying to observe some object property don't forget to ensure the object's class is KVC compliant for that property!
And also don't forget to remove the observers after job is done.
after an research i discovered the sigleton that may help me but i have some problem to understand how it work.
i need that 2 class comunicate each other, here an example:
i have a tableView wich field are dinamic.
this is my code on viewController.h file:
-(void)ottieniMarche{
responseSimulate = [[NSArray alloc]initWithObjects:#"pollo",#"cane",#"gatto",#"verme",#"gallo",#"topo",#"canguro",#"elefante",#"giraffa" ,nil];
}
Now i have to send this information on my mainTableView.m ad the code that i'm actually using is this:
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
elementMainTableView = [[NSArray alloc]initWithObjects:#"aaa",#"bbb",#"ccc",#"ddd",#"eee",#"fff",#"ggg",#"hhh",#"iii" ,nil];
}
return self;
}
Now i have to change the elementMainTableView value with the responseSimulate value. I mean that the content of my maintableView are the same of my responseSimulate.
If I understand your question correctly, you want to refresh the content of the UITableView based on model changes.
UITableView requires a class that conforms to the UITableViewDataSource protocol to provide its row and section data. Often, that's a UITableViewController, but it does not have to be. The data source for your UITableView could the other class to which you refer. In that case, the key is to ask the reload the data, i.e. [tableView reloadData] when you change the data.
In your case, if your MainTableView (I'm inferring this is actually a UITableViewController subclass...) conforms to the UITableViewDataSource protocol, then you could solve the problem using Key-Value Observing for example: (Note, this example assumes you are using ARC.)
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if( !self ) return nil;
elementMainTableView = [[NSArray alloc] initWithObjects:#"aaa",#"bbb",#"ccc",nil];
[self addObserver:self forKeyPath:#"elementMainTableView" options:NSKeyValueObservingOptionNew context:NULL];
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
{
if( [keyPath isEqualToString:#"elementMainTableview"] )
{
[[self tableView] reloadData];
}
}
This assumes that you expose elementMainTableView as a property on MainTableView.
Now, in the other class (?ViewController) your method:
-(void)ottieniMarche {
responseStimulate = [[NSArray alloc] initWithObjects:#"pollo",#"cane",#"gato",nil];
_mainTableViewController.elementMainTableView = responseStimulate;
}
For this to work, you will need your ViewController to keep a reference to the MainTableView, e.g. _mainTableViewController above.
I have an NSTableView, created from IB, that I want to only autohide the horizontal scroller on. The main reason I want to do this is because it seems the NSTableView corverView only get's displayed if there is a vertical scroller.
I can't find any method to do this with the base class. So I tried subclassing NSScrollView and observing the hidden key on the horizontal scroller (code below). This works; however, the view tries to reset the current visible options every time the user resizes the window. This makes my implementation somewhat expensive; and it seems inelegant. Any better ideas about how to do this?
Thanks in advance!
Current implementation:
#interface PVScrollView : NSScrollView {
BOOL autohidesHorizontalScroller;
}
#property(assign) BOOL autohidesHorizontalScroller;
- (void) viewResized:(NSNotification*)notification;
#end
#implementation PVScrollView
#synthesize autohidesHorizontalScroller;
- (void) setAutohidesHorizontalScroller:(BOOL)val
{
autohidesHorizontalScroller = val;
[self setAutohidesScrollers:NO];
[[self horizontalScroller] addObserver:self
forKeyPath:#"hidden"
options:0
context:nil];
}
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (!([self documentVisibleRect].size.width < [[self documentView] frame].size.width) )
{
// remove observer
[[self horizontalScroller] removeObserver:self
forKeyPath:#"hidden"];
[[self horizontalScroller] setHidden:YES];
//[[self horizontalScroller] setNeedsDisplay:YES];
// add it back
[[self horizontalScroller] addObserver:self
forKeyPath:#"hidden"
options:0
context:nil];
}
}
#end
Give this a shot in your NSScrollView subclass:
- (void)setFrameSize:(NSSize)newSize;
{
NSSize minFrameSize = [NSScrollView frameSizeForContentSize:[self contentSize] hasHorizontalScroller:NO hasVerticalScroller:YES borderType:[self borderType]];
BOOL wantScroller = minFrameSize.width > newSize.width;
[self setHasHorizontalScroller:wantScroller];
[super setFrameSize: newSize];
}
You'll need to check "Show Vertical Scroller" and uncheck "Automatically Hide Scrollers" for it to work; I didn't bother making it robust to changes in IB. Also, you'll need to do the same thing when the window is first displayed (in the NSScrollView constructor).
I compared CPU usage with and without this change; it seems to vary at most 1% (19%→20%) in my test application.
I have an NSTextField in my view. Its value is bound to an NSNumber *number in my controller. The controller simply calls through to the model (value) to get the appropriate value.
// In the controller
- (NSNumber *)number {
return [NSNumber numberWithFloat:[model value]];
}
- (void)setNumber:(NSNumber *)aNumber {
[model setValue:[aNumber floatValue]];
}
This is fine, only the controller is not notified of model changes and thus, changing the value in the model does not update the NSTextField.
The only other option I can think of is to have the model notify the controller and the controller manually update the view through an Outlet. But this circumvents the binding.
// In the model, after value change
[[NSNotificationCenter defaultCenter] postNotificationName:#"ValueChanged" object:self];
// In the controller, after being notified
- (void)updateView:(NSNotification *)aNotification {
[myTextField setFloatValue:[model value]];
}
Is there a better, binding-aware way to implement this communication?
I have an NSTextField in my view. Its value is bound to an NSNumber *number in my controller. The controller simply calls through to the model (value) to get the appropriate value.
Why is this property wrapping the value in an NSNumber? KVC will convert primitive values, such as floats, to and from objects for you.
This is fine, only the controller is not notified of model changes and thus, changing the value in the model does not update the NSTextField.
The model changing isn't relevant here, and the controller doesn't need to know about the value of the model's property changing, unless the controller is observing that property—in which case, the controller is getting notified of those changes, because you are using the model's accessors for the property.
The problem is that you have not bound the text field through the model—you're binding it to a fake property on the controller instead.
So, assuming the controller has exposed a property for the model object, simply bind the text field to the controller with the key path model.value.
In your controller, override the keyPathsForValuesAffectingValueForKey: class method. Something like:
+(NSSet*) keyPathsForValuesAffectingValueForKey:(NSString*)key
{
if([key isEqual:#"number"])
return [NSSet setWithObject:#"model.value"];
return [NSSet set];
}
(I'm not at my Mac so this is untested; but the general idea should be sound)
Another option (and what I usually do myself, purely for personal preference) is something like (in the controller):
-(void)setModel:(id)m
{
[model removeObserver:self forKeyPath:#"value"];
[m retain];
[model release];
model = m;
[model addObserver:(self) forKeyPath:#"value" options:NSKeyValueObservingOptionNew context:NULL];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqual:#"value"])
{
[self willChangeValueForKey:#"number"];
[self didChangeValueForKey:#"number"];
}
}
Again, this code is untested, but the general idea should work.