1
votes

I have a window with two NSTextField controls whose values must be kept in sync with one another. Specifically, one is width and the other is length and the ratio of them should remain equal to a given aspect ratio. I want the user to be able to set whichever they prefer and have the other update automagically.

My first approach: I set the delegate for each of the two controls to be the window controller. I implemented the controlTextDidEndEditing method. I check the sender. If it was the width field, I calculated the height field and updated its value via the bound property (self.my_height = self.my_width/aspectRatio). Similarly, if it was the height field, (self.my_width = self.my_height*aspectRatio).

But I hit a snag. Assume that the aspect ratio was 1.5. The user enters 100 in the width field and then moves on to another field. The height field updates to 66 (for better or worse, I chose to always round down). The user clicks in the height field, makes NO change and the clicks elsewhere. The width field is updated to 99. The user is now scratching his/her head.

My refinement. I also implemented the controlTextDidChange method in the window controller. All this does is set a static (file scoped) BOOL variable (gTextFieldDidChange) to YES. Now, when I enter the controlTextDidEndEditing, I check the state of gTextFieldDidChange. If NO, then I return immediately. If YES, then I clear gTextFieldDidChange back to NO and handle the length/width updates.

This seems to be working without any problems for me, but it seems rather "back-door". Is there a cleaner/smarter approach that I'm missing?

Thanks for any/all suggestions. mike

2
I think Cocoa Bindings are exactly what you're looking for ;)HAS
ok... I'm using bindings to set/access the width/height properties in both code and UI. How do I get the timing down to only "sync" the two when the user actually makes a change in one of the two NSTextField controls?MikeMayer67
@MikeMayer68 I made a small demo app using only a delegate method and not bindings since it is a pretty trivial case. If you have questions let me know ;-)HAS

2 Answers

1
votes

The following code does what you want using NSTextFieldDelegate> methods:

@interface HASMainViewController () <NSTextFieldDelegate>
@property (weak) IBOutlet NSTextField *widthTextField;
@property (weak) IBOutlet NSTextField *heightTextField;
@property (weak) IBOutlet NSTextField *ratioLabel;
@end

@implementation HASMainViewController

- (void)awakeFromNib {
    self.widthTextField.delegate = self;
    self.heightTextField.delegate = self;
    self.ratioLabel.stringValue = @"1.777777"; // 16:9 (1920 x 1080)
}

- (void)controlTextDidChange:(NSNotification *)obj {
    if (obj.object == self.widthTextField) {
        // If the width textfield gets changed we want to pass its value and -1 for calculating the height.
        // After that we set the height's text field to the calculated value.
        self.heightTextField.integerValue = [self calculateRatioComponentWithWidth:self.widthTextField.integerValue height:-1 ratio:self.ratioLabel.doubleValue];
    } else if (obj.object == self.heightTextField) {
        // If the width textfield gets changed we want to pass its value and -1 for calculating the height.
        // After that we set the height's text field to the calculated value.
        self.widthTextField.integerValue = [self calculateRatioComponentWithWidth:-1 height:self.heightTextField.integerValue ratio:self.ratioLabel.doubleValue];
    }
}

/**
 *  This method calculates the missing component (determined by "-1") for a given ratio.
 *
 *  @param width  An NSInteger representing the width. Pass -1 if this value should be calculated.
 *  @param height An NSInteger representing the height. Pass -1 if this value should be calculated.
 *  @param ratio  The ratio which needs to be kept.
 *
 *  @return An NSInteger which is depending on the what you passed as parameters the calculated width or height.
 *
 *  @discussion This method raises an HASInternalInconsistencyException exception if both width and height parameters are -1.
 *
 */
- (NSInteger)calculateRatioComponentWithWidth:(NSInteger)width height:(NSInteger)height ratio:(double)ratio {
    if (width == -1 && height == -1) [[NSException exceptionWithName:@"HASInternalInconsistencyException"
                                                              reason:@"Both width and height are -1 (to be calculated)."
                                                            userInfo:nil] raise];
    // Calculate new width
    if (width == -1) return (NSInteger)round(height * ratio);
    // Calculate new width
    else if (height == -1) return (NSInteger)round(width * 1 / ratio);
    // Just to silence the compiler
    return 0;
}

@end

You can clone or download a small sample app from a Bitbucket Repository I've just made for you ;-)

0
votes

I actually found a better and simpler solution to my problem than what I initially attempted and what was suggested by HAS.

I simply checked the 'continuously update value' box under the value binding. Doing this means that the bound value is updated prior to the call to controlTextDidChange.

I can now make the correction as it is typed and no longer need to defer until the invocation of controlTextDidEndEditting.