Integrate NSStepper with NSTextField - cocoa

I need to have a NSTextField working with a NSStepper as being one control so that I can edit an integer value either by changing it directly on the text field or using the stepper up/down arrows.
In IB I've added both of these controls then connected NSStepper's takeIntValueFrom to NSTextField and that makes the text value to change whenever I click the stepper arrows. Problem is that if I edit the text field then click the stepper again it will forget about the value I manually edited and use the stepper's internal value.
What's the best/easiest way to have the stepper's value be updated whenever the text field value is changed?

Skip the takeIntValueFrom: method. Instead, bind both views to the same property in your controller. You may also want to create a formatter and hook up the text field's formatter outlet to it.

I would have a model with one integer variable, which represents the value of both controls.
In my controller, I would use one IBAction, connected to both controls, and two IBOutlets, one for each control. then I would have a method for updating outlets from model value.
IBOutlet NSStepper * stepper;
IBOutlet NSTextField * textField;
- (IBAction) controlDidChange: (id) sender
{
[model setValue:[sender integerValue]];
[self updateControls];
}
- (void) updateControls
{
[stepper setIntegerValue:[model value]];
[textField setIntegerValue:[model value]];
}
This is the principle. As said by Peter Hosey, a formatter may be useful on your text field, at least to take min and max values of stepper into account.

I found easy way is to bind stepper value to input and input value to stepper
#property (strong) IBOutlet NSTextField *timeInput;
#property (strong) IBOutlet NSStepper *timeStepper;

If one is keeping track of the value of a field in one's model, such as a current page number, then there's no need to keep another copy in the stepper control. I just configure the control to have an initial value of 0, and a range from -1 to 1. In the IBAction method for the stepper control, which gets called for any click (or for auto-repeat) on the control, ask for its current value, which will be 1 if the up-arrow was clicked, or -1 for the down-arrow. Immediately reset the control's current value to 0, and then update the model and anything else (the associated text field, or a new page view, etc.) with a new value based on the direction 1 or -1. E.g.,
- (IBAction) bumpPageNum:(id)sender
{
int whichWay = [sender intValue]; // Either 1 or -1
[sender setIntValue:0]; // Same behavior next time
[model changePageBy:whichWay];
}
This way, the stepper control doesn't have to be linked to any values in the model at all.

I did as Peter Hosey suggested as it seems to me the cleanest approach. Created a property in my controller:
int editValue_;
...
#property (nonatomic, readwrite) int editValue;
...
#synthesize editValue = editValue_;
then in IB for both controls in the Bindings tab I've set the "Bind to:" check box and selected my controller, then on the "Model Key Path" field set "editValue" and voilá, it worked! With just 3 lines of code and some IB editing. And if I need to change the value on my controller I use setEditValue: and the text field gets updated.

This is for people who care about Cocoa.
The only reason to use NSStepper together with NSTextField is because there is some number in the textfield.
Steps for complete advanced Cocoa solution (which is sadly missing here):
Step 1: add number formatters to your textfields and format as you wish.
Step 2: add NSObjectController and glue your textfields/steppers to it. This is a common mistake when people do direct bindings. Meh. Add respective keyPaths as you have in your model.
Step 3: make sure your textfields react to key events. Always missing by newbies. Hook textfield delegate to our controller and add code.
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
{
if (commandSelector == #selector(moveUp:) || commandSelector == #selector(moveDown:)) {
if (control == [self minAgeTextField]) {
return [[self minAgeStepper] sendAction:commandSelector to:[self minAgeStepper]];
}
if (control == [self maxAgeTextField]) {
return [[self maxAgeStepper] sendAction:commandSelector to:[self maxAgeStepper]];
}
}
return NO;
}
Step 4: Some glue code. This is also the place where we set content of our objectController.
#property (weak) IBOutlet NSObjectController *profilesFilterObjectController;
#property (weak) IBOutlet NSTextField *minAgeTextField;
#property (weak) IBOutlet NSTextField *maxAgeTextField;
#property (weak) IBOutlet NSStepper *minAgeStepper;
#property (weak) IBOutlet NSStepper *maxAgeStepper;
#property (nonatomic) ProfilesFilter *filter;
- (void)awakeFromNib //or viewDidLoad...
{
[self setFilter:[ProfilesFilter new]];
[[self profilesFilterObjectController] setContent:[self filter]];
}
Step 5: Validate your values (KVC validation)
#implementation ProfilesFilter
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError
{
if ([inKey isEqualToString:#"minAge"] || [inKey isEqualToString:#"maxAge"]) {
if (*ioValue == nil) {
return YES;
}
NSInteger minAge = [[self minAge] integerValue];
NSInteger maxAge = [[self maxAge] integerValue];
if ([inKey isEqualToString:#"minAge"]) {
if (maxAge != 0) {
*ioValue = #(MAX(18, MIN([*ioValue integerValue], maxAge)));
}
}
if ([inKey isEqualToString:#"maxAge"]) {
if (minAge != 0) {
*ioValue = #(MIN(99, MAX([*ioValue integerValue], minAge)));
}
}
}
return YES;
}
#end
Notes: Wrong values? NSNumberFormatter will show error. Max age lower than min age? We use KVC validation (step 5). Eureka!
BONUS: What if user holds CTRL or SHIFT or both (user wants slower or faster increment)? We can modify increment based on key pressed (subclass NSStepper and overrider increment getter and check e.g. NSEvent.modifierFlags.contains(.shift)).
- (double)increment
{
BOOL isShiftDown = ([NSEvent modifierFlags] & NSEventModifierFlagShift) ? YES : NO;
//BOOL isOptionDown = ([NSEvent modifierFlags] & NSEventModifierFlagOption) ? YES : NO;
double increment = ([self defaultIncrement] - 0.001 > 0) ? [self defaultIncrement] : 1.0;
if (isShiftDown) {
increment = increment * 5;
}
return increment;
}
Add this to - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
//JUST AN ILLUSTRATION, holding shift + key up doesn't send moveUp but moveUpAndModifySelection (be careful of crash, just modify the command to moveUp; if not `NSStepper` doesn't know `moveUpAndModifySelection`)
if (commandSelector == #selector(moveUpAndModifySelection:)) {
commandSelector = #selector(moveUp:);
}
if (commandSelector == #selector(moveToEndOfDocument:) || commandSelector == #selector(moveDownAndModifySelection:)) {
commandSelector = #selector(moveDown:);
}
PS: The best solution is to use custom NSTextField that has and draws stepper and controls all events. You end up with a smaller controller!

Related

Xcode set Bool with buttons

I am trying to create a simple set of buttons which show/hide a text field.
I created a BOOL in .h;
BOOL showBool;
#property BOOL showBool;
Then have buttons linked to actions in .h:
- (IBAction) showText:(id)sender;
- (IBAction) hideText:(id)sender;
Then in .m, I have the actions, which should be setting the bool;
- (IBAction) showText:(id)sender;
{
showBool = YES;
}
- (IBAction) hideText:(id)sender;
{
showBool = NO;
}
I have a text field key value bound via App Delegate as 'hidden' to showBool. But it does not change (hide/show) during run...
Am I setting the booleans incorrectly ??
Bindings rely on key value observing. Use the accessor self.showBool = YES; to trigger the change for the binding, rather than directly giving the ivar a value. This assumes you did the #synthesize in your implementation.

NSTextField with NSNumberFormatter - not allow nil value

I have a NSTextField with a NSNumberFormatter. I set the formatter with a min of 0 (because I couldn't set it to 0.01), and the style to decimal. The NSTextField has a binding on its value with a float ivar, and the action is set to "Send On Enter Only". This works just fine.
What I'd like to do is if the user tries to erase the value and either clicks off, or presses enter, I want to restore the original value before editing.
I tried:
-(void) setNilValueForKey:(NSString*) key {
if ([key compare:#"valX"] == NSOrderedSame) {
self.valX = valX;
}
}
But this doesn't set the NSTextField. I'm at a loss, any help is appreciated.
Thanks
GW
For case of erasing, Implement the following delegate method as
- (void)controlTextDidChange:(NSNotification *)notification
{
NSTextField* textfield = [notification object];
if([textfield intValue] == 0)
{
[textfield setValue:valX];
}
}
For case of pressing Enter and clicking off, implement
- (void)controlTextDidEndEditing:(NSNotification *)obj
{
NSTextField* textfield = [notification object];
[textfield setValue:valX];
}

MapKit changes annotation view order when map is scrolled in XCode 4.6

I am an experienced iOS developer, but this has really stumped me. I am simultaneously submitting a problem report to Apple.
I'm adding annotations to a MKMapKit map (Xcode 4.6). Each annotation is a MyAnnotation class; MyAnnotation defines a property, location_id, that I use to keep track of that annotation.
The problem is simple: I want the MyAnnotation with a location_id of -1 to appear in front of everything else.
To do this, I am overriding mapView:didAddAnnotationViews: in my MKMapViewDelegate:
-(void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
// Loop through any newly added views, arranging them (z-index)
for (MKAnnotationView* view in views) {
// Check the location ID
if([view.annotation isKindOfClass:[MyAnnotation class]] && [((MyAnnotation*)(view.annotation)).location_id intValue]==-1 ) {
// -1: Bring to front
[[view superview] bringSubviewToFront:view];
NSLog(#"to FRONT: %#",((MyAnnotation*)view.annotation).location_id);
} else {
// Something else: send to back
[[view superview] sendSubviewToBack:view];
NSLog(#"to BACK: %#",((MyAnnotation*)view.annotation).location_id);
}
}
}
this works just fine. I have an "add" button that adds an annotation to a random location near the center of my map. Each time I push the "add" button, a new annotation appears; but nothing hides the annotation with the location_id of -1.
** UNTIL ** I scroll!
As soon as I start scrolling, all of my annotations are rearranged (z-order) and my careful stacking no longer applies.
The really confusing thing is, I've done icon order stacking before with no problem whatsoever. I've created a brand new, single view app to test this problem out; sending MKAnnotationView items to the back or front only works until you scroll. There is nothing else in this skeleton app except the code described above. I'm wondering if there is some kind of bug in the latest MapKit framework.
The original problem was in trying to add new annotations when the user scrolled (mapView:regionDidChangeAnimated:). The annotation adds; the mapView:didAddAnnotationViews: code fires; and then the order is scrambled by an unseen hand in the framework (presumably as the scroll completes).
In case you're interested, here is my viewForAnnotation:
-(MKAnnotationView*)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
// Is this an A91Location?
if([annotation isKindOfClass:[MyAnnotation class]]){
MyAnnotation* ann=(MyAnnotation*)annotation;
NSLog(#"viewForAnnotation with A91Location ID %#",ann.location_id);
if([ann.location_id intValue]==-1){
// If the ID is -1, use a green pin
MKPinAnnotationView* green_pin=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:nil];
green_pin.pinColor=MKPinAnnotationColorGreen;
green_pin.enabled=NO;
return green_pin;
} else {
// Otherwise, use a default (red) pin
MKPinAnnotationView* red_pin=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:nil];
red_pin.enabled=NO;
return red_pin;
}
}
// Everything else
return nil;
}
And my class:
#interface MyAnnotation : NSObject <MKAnnotation>
#property (strong, nonatomic, readonly) NSNumber* location_id;
#property (strong, nonatomic, readonly) NSString* name;
#property (strong, nonatomic, readonly) NSString* description;
#property (nonatomic) CLLocationCoordinate2D coordinate;
-(id) initWithID:(NSNumber*)location_id name: (NSString*) name description:(NSString*) description location:(CLLocationCoordinate2D) location;
// For MKAnnotation protocol... return name and description, respectively
-(NSString*)title;
-(NSString*)subtitle;
#end
Could you possibly try the following as a work around:
1) Remove your code in mapView:didAddAnnotationViews:
2) Pickup when the map is moved or pinched (I assume this is what you consider to be a scroll right?) - I do this with gestures rather than the mapView regionChanged as I have had bad experiences such as unexplained behaviour with the latter.
//recognise the paning gesture to fire the didDragMap method
UIPanGestureRecognizer* panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(didDragMap:)];
[panRec setDelegate:self];
[self.mapView addGestureRecognizer:panRec];
//recognise the pinching gesture to fire the didPinchMap method
UIPinchGestureRecognizer *pinchRec = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:#selector(didPinchMap:)];
[pinchRec setDelegate:self];
//[pinchRec setDelaysTouchesBegan:YES];
[self.mapView addGestureRecognizer:pinchRec];
//recognise the doubleTap
UITapGestureRecognizer *doubleTapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didPinchMap:)];
[doubleTapRec setDelegate:self];
doubleTapRec.numberOfTapsRequired = 2;
[self.mapView addGestureRecognizer:doubleTapRec];
3) write a custom "plotAnnotations" function to show your annotations. This function will loop through all your annotations and save them in an array "annotationsToShow" ordered by your location id DESC so that your location_id -1 are added last. Use [self.mapView addAnnotations:annotationsToShow]; to display them.
4) Call plotAnnotations in your gestureRecognizer functions
- (void)didDragMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self plotAnnotations];
}
}
- (void)didPinchMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self plotAnnotations];
}
}
5) You may need to delete all annotations before displaying the new ones in 3) [self.mapView removeAnnotations:annotationsToRemove];

Button Crashes App with (id) sender

I am attempting to make a basic game which requires a serious of buttons to control player movement. Keep in mind I am using cocos-2d. My goal is to have the buttons be holdable and move a sprite when held down. The code i am using now looks like this.
CCMenuItemHoldable.h
#interface CCMenuItemSpriteHoldable : CCMenuItemSprite {
bool buttonHeld;
}
#property (readonly, nonatomic) bool buttonHeld;
CCMenuItemHoldable.m
#implementation CCMenuItemSpriteHoldable
#synthesize buttonHeld;
-(void) selected
{
[super selected];
buttonHeld = true;
[self setOpacity:128];
}
-(void) unselected
{
[super unselected];
buttonHeld = false;
[self setOpacity:64];
}
#end
and for the set up of the buttons
rightBtn = [CCMenuItemSpriteHoldable itemFromNormalSprite:[CCSprite spriteWithFile:#"art/hud/right.png"] selectedSprite:[CCSprite spriteWithFile:#"art/hud/right.png"] target:self selector:#selector(rightButtonPressed)];
CCMenu *directionalMenu = [CCMenu menuWithItems:leftBtn, rightBtn, nil];
[directionalMenu alignItemsHorizontallyWithPadding:0];
[directionalMenu setPosition:ccp(110,48)];
[self addChild:directionalMenu];
This all seems to work fine but when i do
-(void)rightButtonPressed:(id) sender
{
if([sender buttonHeld])
targetX = 10;
else{
targetX = 0;
}
}
The crash has been fixed but I am trying to get my sprite to move. In my game tick function I add the value of targetX to the position of the sprite on a timer, still no movement.
Please, always include a crash log when asking questions about crashes.
In your case, I can guess the problem. You are adding this selector:
#selector(rightButtonPressed)
Your method is called
rightButtonPressed:(id)sender
Which would be, as a selector, rightButtonPressed: - note the colon indicating that an argument is passed. Either change the method so it has no argument, or add a colon to the selector when you create the button.
The crash log would be telling you this - it would say "unrecognised selector sent to..." with the name of the receiving class, and the name of the selector.

Get keyDown event for an NSTextField

In xcode last version, I am trying to get the keyDown event of an NSTextField.
However, despite following multiple tutorials on the internet (delegates, controllers...), I still can't receive it.
Any easy hint for me ?
Thanks !
I got sick of all the non answers to do it some other way people so I put my nose down and figured out a way to make this work. This isn't using keydown event directly but it is using the keydown in the block. And the behavior is exactly what I wanted.
Subclass the text field
.h
#interface LQRestrictedInputTextField : NSTextField
.m
In the become first responder setup a local event
static id eventMonitor = nil;
- (BOOL)becomeFirstResponder {
BOOL okToChange = [super becomeFirstResponder];
if (okToChange) {
[self setKeyboardFocusRingNeedsDisplayInRect: [self bounds]];
if (!eventMonitor) {
eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^(NSEvent *event) {
NSString *characters = [event characters];
unichar character = [characters characterAtIndex:0];
NSString *characterString=[NSString stringWithFormat:#"%c",character];
NSArray *validNonAlphaNumericArray = #[#" ",#"(",#")",#"[",#"]",#":",#";",#"\'",#"\"",#".",#"<",#">",#",",#"{",#"}",#"|",#"=",#"+",#"-",#"_",#"?",#"#",
#(NSDownArrowFunctionKey),#(NSUpArrowFunctionKey),#(NSLeftArrowFunctionKey),#(NSRightArrowFunctionKey)];
if([[NSCharacterSet alphanumericCharacterSet] characterIsMember:character] || character == NSCarriageReturnCharacter || character == NSTabCharacter || character == NSDeleteCharacter || [validNonAlphaNumericArray containsObject:characterString ] ) { //[NSCharacterSet alphanumericCharacterSet]
} else {
NSBeep();
event=nil;
}
return event;
} ];
}
}
NSLog(#"become first responder");
return okToChange;
}
remove the event once the textfield editing ends
Also if you're using ARC I noticed you might need to assign the textview string to the stringValue. I nslog'd the stringValue and the value was retained. Without the nslog I had to assign the notification object string to the stringValue to keep it from getting released.
-(void) textDidEndEditing:(NSNotification *)notification {
[NSEvent removeMonitor:eventMonitor];
eventMonitor = nil;
NSTextView *textView=[notification object];
self.stringValue=textView.string;
}
You can subclass NStextField and use keyUp that works for NSTextField.
in .h
#interface MyTextField : NSTextField <NSTextFieldDelegate>
in .m
-(void)keyUp:(NSEvent *)theEvent {
NSLog(#"Pressed key in NStextField!");
}
Add UITextFieldDelegate to your .h like this
#interface ViewController : UIViewController <UITextFieldDelegate> {
Then you can use this to detect every key press in a text field
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
Return YES to allow the character that was pressed to be inserted into the field but you can add whatever code you need in here.

Resources