cocoanstextfieldnscontrol

Coordinating a pair of NSTextField controls


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


Solution

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