问题
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?
回答1:
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.
回答2:
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.
回答3:
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;
回答4:
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.
回答5:
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.
回答6:
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!
来源:https://stackoverflow.com/questions/702829/integrate-nsstepper-with-nstextfield