Getting the value of the changed property related to an Observer - objective-c

I've got an observer on my textFields which looks to see if the "enabled# property has changed.
(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *) context;
{
UITextField *txtField = (UITextField *)object;
BOOL new = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
BOOL old = [[change objectForKey:NSKeyValueChangeOldKey] boolValue];
if ((new != old) && (new = YES))
{
[self fadeDisable:txtField];
}
else if ((new != old) && (new = NO))
{
[self fadeEnable:txtField];
}
I thought if I used int new and int old, the 1 or 0 which defined if the property is enabled or not would be returned but when I use NSLog to see what thy are bringing back, it's a long string of numbers.
I looked through the documentation and it seems that objectForKey actually return an id not an integer but i'm not sure what to do.
Edit: i've added the code for my comparison which is trying to determine if it went from disabled to enabled (or vice versa). Also added the boolValue correction as recommended.
It does not give the intended result and doesn't call the correct method. Is it correct?
Thanks

NSDictionary contains objects (like NSNumber), not primitive types (like int). As you have noticed,
[change objectForKey:NSKeyValueChangeNewKey]
returns id. If you want to convert it to int, use
int new = [[change objectForKey:NSKeyValueChangeNewKey] intValue]
Or if the property is a BOOL, even better:
BOOL new = [[change objectForKey:NSKeyValueChangeNewKey] boolValue]
This line of code
int new = [change objectForKey:NSKeyValueChangeNewKey]
results in storing value of pointer to the NSNumber object into new integer, this is the "long string of numbers" you mentioned. Strangely, it does compile without even a warning.

Related

How can I optimize this huge if/else if block within observeValueForKey

I have a controller that is registered as an observer for a LOT of properties on views. This is our -observeValueForKeyPath:::: method:
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void*)context
{
if( context == kStrokeColorWellChangedContext )
{
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kStrokeColorProperty];
}
else if( context == kFillColorWellChangedContext )
{
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kFillColorProperty];
}
else if( context == kBodyStyleNumChangedContext )
{
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kBodyStyleNumProperty];
}
else if( context == kStyleChangedContext )
{
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kStyleProperty];
}
else if( context == kStepStyleChangedContext )
{
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kStepStyleProperty];
}
else if( context == kFirstHeadStyleChangedContext )
{
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kFirstHeadStyleProperty];
}
else if( context == kSecondHeadStyleChangedContext )
{
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kSecondHeadStyleProperty];
}
And there's actually about 3x more of these else if statements.
One thing you can see is that each block has the same code, which makes me think that it's possible to optimize this.
My initial thought was to have an NSDictionary called keyPathForContextDictionary where the keys are the constants with the Context suffix (of type void*), and the values are the appropriate string constants, denoted by the Property suffix
Then this method would only need one line:
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:keyPathForContextDictionary[context]];
Note that I need to use a data structure of some sort to identify which keyPath to use, and I can't simply use the keyPath argument passed into the method. This is because there are multiple views that have the same property I'm observing (for example, color wells have the color property). So each view needs to determine a unique keypath, which is currently being determined based off of the context
The problem with this is that you cannot use void* as keys in an NSDictionary. So... does anybody have any recommendations for what I could do here?
EDIT:
Here's an example of how the constants are defined:
void * const kStrokeColorWellChangedContext = (void*)&kStrokeColorWellChangedContext;
void * const kFillColorWellChangedContext = (void*)&kFillColorWellChangedContext;
void * const kBodyStyleNumChangedContext = (void*)&kBodyStyleNumChangedContext;
void * const kStyleChangedContext = (void*)&kStyleChangedContext;
NSString *const kStrokeColorProperty = #"strokeColor";
NSString *const kFillColorProperty = #"fillColor";
NSString *const kShadowProperty = #"shadow";
NSString *const kBodyStyleNumProperty = #"bodyStyleNum";
NSString *const kStyleProperty = #"style";
The type void * is not so much a type unto itself that you have to match, as it is "generic pointer". It's used for the context argument precisely so that you can use any underlying type that you like, including an object type. All you have to do is perform the proper casts.
You can therefore change your kTHINGYChangedContexts to be NSStrings or any other object you like very easily, and then use them as keys in your context->key path mapping.
Start with:
NSString * const kStrokeColorWellChangedContext = #"StrokeColorWellChangedContext";
When you register for observation, you must perform a bridged cast:
[colorWell addObserver:self
forKeyPath:keyPath
options:options
context:(__bridge void *)kStrokeColorWellChangedContext];
Then when the observation occurs, you do the reverse cast:
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void*)ctx
{
NSString * context = (__bridge NSString *)ctx;
// Use context, not ctx, from here on.
}
And proceed to your key path lookup from there.
Josh Caswell had a great answer, but I didn't want to modify the type of our constants into NSStrings*
So a solution instead, was to cast the void* into NSValues w/ -valueWithPointer. This way I could use the void* as keys in my dictionary
Here's the code:
NSString *toolKeyPath = [[ToolController keyPathFromContextDictionary] objectForKey:[NSValue valueWithPointer:context]];
if( toolKeyPath )
{
if( [change objectForKey:NSKeyValueChangeNewKey] == (id)[NSNull null] )
{
[self setValue:nil forKey:toolKeyPath];
}
else
{
[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:toolKeyPath];
}
}
And the dictionary:
+(NSDictionary*) keyPathFromContextDictionary
{
return #{
[NSValue valueWithPointer:kStrokeColorWellChangedContext] : kStrokeColorProperty,
[NSValue valueWithPointer:kFillColorWellChangedContext] : kFillColorProperty,
[NSValue valueWithPointer:kBodyStyleNumChangedContext] : kBodyStyleNumProperty,
[NSValue valueWithPointer:kStyleChangedContext] : kStyleProperty,
[NSValue valueWithPointer:kStepStyleChangedContext] : kStepStyleProperty,
[NSValue valueWithPointer:kFirstHeadStyleChangedContext] : kFirstHeadStyleProperty,
[NSValue valueWithPointer:kSecondHeadStyleChangedContext] : kSecondHeadStyleProperty,
[NSValue valueWithPointer:kShadowChangedContext] : kShadowProperty,
[NSValue valueWithPointer:kStrokeWidthChangedContext] : kStrokeWidthProperty,
[NSValue valueWithPointer:kBlurRadiusChangedContext] : kBlurRadiusProperty,
[NSValue valueWithPointer:kFontSizeChangedContext] : kFontSizeProperty
};
}

NSTokenField not firing action

I have an NSTokenField to add tags to an object (a document). I would like to update the object with new tags the moment a token is added to the token field (when a tokenising character is typed). Unfortunately this does not seem to work.
The NSTokenField is connected to an action in my controller but this action method is never called.
I also have a NSTextField connected in the same way to the controller and its action method in the controller is called.
I've also tried this with key value observing:
- (void) awakeFromNib {
[tokenField addObserver:self forKeyPath:#"objectValue" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if([object isEqual:tokenField]){
NSLog(#"Tokens changed");
}
}
but this action is only called when I programatically change the tokens.
How can I be notified when the tokens in the tokenField are changed?
The NSTokenField action selector isn't called the moment a new tag is created. Depending on the setting you've gone with in Interface Builder, it's called either when you hit enter to end editing (Send On Enter Only) , or when you end editing some other way (Send On End Editing). To get the fine control you're after you'll need another approach.
The blue tags that appear when a tokenising character is added to the token field are called text attachments (instances of NSTextAttachment). One way of working out when tags are being added/removed from your token field is to track changes to the number of these objects contained in the token field's underlying attributed string.
To get access to the relevant attributed string you need to get hold of the fieldEditor's layoutManager - the object which ultimately supplies the string that appears in the text-view. Once you've got it, each time you get a controlTextDidChange: message, count up the number of text attachments in the string representation of its attributedString. If the number this time around is greater than the number recorded in the previous count, a tag has just been added.
#import "AppDelegate.h"
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#property (weak) NSLayoutManager *lm;
#property (nonatomic) NSUInteger tokenCount;
#end
#implementation AppDelegate
// The text in the fieldEditor has changed. If the number of attachments in the
// layoutManager's attributedString has changed, either a new tag has been added,
// or an existing tag has been deleted.
-(void)controlTextDidChange:(NSNotification *)obj {
NSUInteger updatedCount = [self countAttachmentsInAttributedString:self.lm.attributedString];
if (updatedCount > self.tokenCount) {
NSLog(#"ADDED");
self.tokenCount = updatedCount;
} else if (updatedCount < self.tokenCount) {
NSLog(#"REMOVED");
self.tokenCount = updatedCount;
}
}
// About to start editing - get access to the fieldEditor's layoutManager
-(BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor {
self.lm = [(NSTextView *)fieldEditor layoutManager];
return YES;
}
// Iterate through the characters in an attributed string looking for occurrences of
// the NSAttachmentCharacter.
- (NSInteger)countAttachmentsInAttributedString:(NSAttributedString *)attributedString {
NSString *string = [attributedString string];
NSUInteger maxIndex = string.length - 1;
NSUInteger counter = 0;
for (int i = 0; i < maxIndex + 1; i++) {
if ([string characterAtIndex:i] == NSAttachmentCharacter) {
counter++;
}
}
return counter;
}
#end
A port of #paul-patterson's code to Swift 3:
override func controlTextDidChange(_ obj: Notification) {
guard let fieldEditor = self.tokenField.currentEditor() as? NSTextView,
let layoutManager = fieldEditor.layoutManager
else { return }
func countAttachments(attributedString: NSAttributedString) -> Int {
let string = attributedString.string as NSString
let maxIndex = string.length - 1
var counter = 0
for i in 0..<maxIndex {
if string.character(at: i) == unichar(NSAttachmentCharacter) {
counter += 1
}
}
return counter
}
let currentCount = countAttachments(attributedString: layoutManager.attributedString())
// cache count or act on it directly
}
Oddly enough, the following does not produce the expected outcome in Swift:
layoutManager.attributedString().string
.split(by: Character(UnicodeScalar(NSAttachmentCharacter)!)).count
Instead, it returns 0 when the user is not typing and 1 when a token is being edited.
let isEditing = layoutManager.attributedString().string
.split(by: Character(UnicodeScalar(NSAttachmentCharacter)!)).count == 1
With the combination of both approaches, you could write a custom "did add/remove token" callback using a state machine. (I don't think this is a very safe way to implement that, though.)
Track the count of tokens with countAttachments(attributedString:).
Use isEditing to check ...
if the user started adding a new note (new count > old count && isEditing == true)
if the user started editing an existing note (new count == old count && isEditing == true)
if the user finished a token (oldIsEditing == true && newIsEditing == false)

How can I force NSTextField to only allow numbers?

In Interface Builder I’ve created a textfield and stuck an NSNumberFormatter into the cell. However, the user can still type text into the textfield. Is there any way to restrict the input to numbers only using interface builder? I thought that was the point of the NSNumberFormatter.
Every formatter has this method:
- (BOOL) getObjectValue: (id*) object forString: (NSString*) string
errorDescription: (NSError**) error;
This is valid for every formatter.
If the string is not valid it returns false and the object passed as argument (dereferenced) will be nil.
So instead of dropping a number formatter to the text field, use your own formatter as instance variable.Observe the text field implementing the delegate protocol so that at every change of the text you can be notified.
Then invoke this method:
NSNumber* number;
BOOL success=[formatter getObjectValue: &number forString: [myTextField stringValue] errorDescription:nil];
At this point if success is false (or check if number is nil), there is an invalid string in the text field, so do the action that is more appropriate for you (maybe delete the entire string, or display 0.0 as value).
There is also another method:
- (BOOL) isPartialStringValid : (NSString*) partialString: (NSString*) partialString
errorDescription: (NSString*) error;
This way you can know if a partial string is valid.For example with the scientific notation "1e" is not valid, but is partially valid because the user may add 1 and it will become "1e1".
Create an NSNumberFormatter subclass and put this in the implementation. In your code, set the YKKNumberFormatter as the formatter for the NSTextField/UITextField.
#implementation YKKNumberFormatter
- (BOOL)isPartialStringValid:(NSString *)partialString newEditingString:(NSString **)newString errorDescription:(NSString **)error {
// Make sure we clear newString and error to ensure old values aren't being used
if (newString) { *newString = nil;}
if (error) {*error = nil;}
static NSCharacterSet *nonDecimalCharacters = nil;
if (nonDecimalCharacters == nil) {
nonDecimalCharacters = [[[NSCharacterSet decimalDigitCharacterSet] invertedSet] retain];
}
if ([partialString length] == 0) {
return YES; // The empty string is okay (the user might just be deleting everything and starting over)
} else if ([partialString rangeOfCharacterFromSet:nonDecimalCharacters].location != NSNotFound) {
return NO; // Non-decimal characters aren't cool!
}
return YES;
}
#end

NSFormatter for BOOL

I have set up my simple Xcode project with a table that is binded to an array controller. It works fine if the array controller is full of entities with a string attribute. However I want to change the attribute to a BOOL and have the table show the string "true" or "false" based on the BOOL.
I have overrided the following two methods from NSFormatter:
-(NSString*) stringForObjectValue:(id)object {
//what is the object?
NSLog(#"object is: %#", object);
if(![object isKindOfClass: [ NSString class ] ] ) {
return nil;
}
//i'm tired....just output hello in the table!!
NSString *returnStr = [[NSString alloc] initWithFormat:#"hello"];
return returnStr;
}
-(BOOL)getObjectValue: (id*)object forString:string errorDescription:(NSString**)error {
if( object ) {
return YES;
}
return NO;
}
So the table gets populated with "hello" if the attribute is a string however if I switch it to a boolean, then the table gets populated with lots of blank spaces.
I don't know if this helps but on the line where I'm outputting the object, it outputs __NSCFString if the attribute is a string and "Text Cell" if I switch the attribute to a boolean. This is something else I don't understand.
Ok, it's not 100% clear what you're trying to do from the code, but first things first - BOOL is not an object, it's basically 0 or 1, so to place BOOL values into an array, you're probably best off using NSNumber:
NSNumber *boolValue = [NSNumber numberWithBool:YES];
and placing these into your array. Now you want to change your method:
-(NSString*) stringForObjectValue:(id)object {
NSNumber *number = (NSNumber *)object;
if ([number boolValue] == YES)
return #"true";
else
return #"false";
}
There's a few things here - for example, you want to avoid passing around id references if you can (if you know all your objects in the NSArray are NSNumber, you shouldn't need to).

How to add a binding programmatically for a NSTabView?

My application contains an NSTabView with two tabs. Further, the application itself has a playState which is an enum. The playState is kept in a Singleton.
typedef enum {
kMyAppPlayStatePlaying,
kMyAppPlayStatePaused
} MyAppPlayState;
The playState gets synthesized here.
#property (readwrite) MyAppPlayState playState;
I want to switch the NSTabView every time the playState changes. Therefore, I prepared an IBOutlet to add a binding similar to this one.
[self.playPauseTabView bind:#"selectedItemIdentifier" toObject:[MyAppState sharedState] withKeyPath:#"playState" options:nil];
I already recognized the identifier must be NSString. This does not match with my enum which is an int. I could maybe use an NSValueTransformer to fix this.
Further, selectedItemIdentifier does not exists. NSTabView only offers selectedTabViewItem which then allows to access identifier or label. Though, I cannot find a way to switch the item itself based on the identifer.
In situations like that, I find myself doing one of two things:
1) Register self (or some other object) as an observer of the property in question, and set the selected tab accordingly in -observeValueForKeyPath:ofObject:change:context:. It could look like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ( context == PlayStateChange )
{
if ( [[change objectForKey: NSKeyValueChangeKindKey] integerValue] == NSKeyValueChangeSetting )
{
NSNumber *oldValue = [change objectForKey: NSKeyValueChangeOldKey];
NSNumber *newValue = [change objectForKey: NSKeyValueChangeNewKey];
NSInteger oldInteger = [oldValue integerValue];
NSInteger newInteger = [newValue integerValue];
NSLog(#"Old play state: %ld, new play state: %ld", (long)oldInteger, (long)newInteger);
// Do something useful with the integers here
}
return;
}
}
2) declare a readonly NSString * property and declare that its value is affected by your playState property. Something like this:
#property (readonly) NSString *playStateStr;
// Accessor
-(NSString *)playStateStr
{
return playState == kMyAppPlayStatePlaying ? #"playing" : "paused";
}
+(NSSet *)keyPathsForValuesAffectingPlayStateStr
{
return [NSSet setWithObject: #"playState"];
}
Now you have an NSString-typed property that you can bind your tab view's selection.
I forgot to connect the NSTabView with its IBOutlet in the Interface Builder.
The following works for me.
NSDictionary* playStateOptions = [NSDictionary dictionaryWithObject:[[PlayStateValueTransformer alloc] init] forKey:NSValueTransformerBindingOption];
[self.playPauseTabView bind:#"selectedLabel" toObject:[MyAppState sharedState] withKeyPath:#"playState" options:playStateOptions];
In the NSValueTransformer I return an NSString which must be set in Interface Builder for each tab!