1

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

MikeMayer67
  • 570
  • 6
  • 17
  • I think [Cocoa Bindings](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaBindings/CocoaBindings.html) are exactly what you're looking for ;) – HAS Dec 02 '13 at 16:28
  • 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 Dec 02 '13 at 16:43
  • @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 Dec 03 '13 at 21:22

2 Answers2

1

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 ;-)

HAS
  • 19,140
  • 6
  • 31
  • 53
  • Ahh... And I was so close, but missed the obvious. I was attempting to use the current value of the width (or height) property to set the other. This failed because it is not updated until just before controlTextdidEndEditting is invoked. I feel like a dolt for not thinking to grab the value out of the control itself. – MikeMayer67 Dec 04 '13 at 12:43
  • Yeah, you were very close! – HAS Dec 04 '13 at 13:31
  • 1
    I don't seem to have sufficient reputation to upvote the answer. But I did accept it. – MikeMayer67 Dec 05 '13 at 06:08
0

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.

MikeMayer67
  • 570
  • 6
  • 17