views:

1642

answers:

4

Need to have an NSTextField with a text limit of 4 characters maximum and show always in upper case but can't figure out a good way of achieving that. I've tried to do it through a binding with a validation method but the validation only gets called when the control loses first responder and that's no good.

Temporarly I made it work by observing the notification NSControlTextDidChangeNotification on the text field and having it call the method:

- (void)textDidChange:(NSNotification*)notification {
  NSTextField* textField = [notification object];
  NSString* value = [textField stringValue];
  if ([value length] > 4) {
    [textField setStringValue:[[value uppercaseString] substringWithRange:NSMakeRange(0, 4)]];
  } else {
    [textField setStringValue:[value uppercaseString]];
  }
}

But this surely isn't the best way of doing it. Any better suggestion?

+5  A: 

Have you tried attaching a custom NSFormatter subclass?

Graham Lee
A: 

The custom NSFormatter that Graham Lee suggested is the best approach.

A simple kludge would be to set your view controller as the text field's delegate then just block any edit that involves non-uppercase or makes the length longer than 4:

- (BOOL)textField:(UITextField *)textField
    shouldChangeCharactersInRange:(NSRange)range
    replacementString:(NSString *)string
{
    NSMutableString *newValue = [[textField.text mutableCopy] autorelease];
    [newValue replaceCharactersInRange:range withString:string];

    NSCharacterSet *nonUppercase =
        [[NSCharacterSet uppercaseLetterCharacterSet] invertedSet];
    if ([newValue length] > 4 ||
        [newValue rangeOfCharacterFromSet:nonUppercase].location !=
            NSNotFound)
    {
       return NO;
    }

    return YES;
}
Matt Gallagher
+5  A: 

I did as Graham Lee suggested and it works fine, here's the custom formatter code:

UPDATED: Added fix reported by Dave Gallagher. Thanks!

@interface CustomTextFieldFormatter : NSFormatter {
  int maxLength;
}
- (void)setMaximumLength:(int)len;
- (int)maximumLength;

@end

@implementation CustomTextFieldFormatter

- init {
  [super init];
  maxLength = INT_MAX;
  return self;
}

- (void)setMaximumLength:(int)len {
  maxLength = len;
}

- (int)maximumLength {
  return maxLength;
}

- (NSString *)stringForObjectValue:(id)object {
  return (NSString *)object;
}

- (BOOL)getObjectValue:(id *)object forString:(NSString *)string errorDescription:(NSString **)error {
  *object = string;
  return YES;
}

- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
   proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
          originalString:(NSString *)origString
   originalSelectedRange:(NSRange)origSelRange
        errorDescription:(NSString **)error {
    if ([*partialStringPtr length] > maxLength) {
        return NO;
    }

    if (![*partialStringPtr isEqual:[*partialStringPtr uppercaseString]]) {
      *partialStringPtr = [*partialStringPtr uppercaseString];
      return NO;
    }

    return YES;
}

- (NSAttributedString *)attributedStringForObjectValue:(id)anObject withDefaultAttributes:(NSDictionary *)attributes {
  return nil;
}

@end
carlosb
You should accept Grahams answer, as he pointed you in the correct direction! Good Job tho!
Jab
Thanks for taking the time to come back and post the whole solution!
Matt Gallagher
I discovered an error with the above code. There's a potential exploit using isPartialStringValid:newEditingString:errorDescription:. If you enter text into an NSTextField, character by character on the keyboard, no issues will arise. However, if you paste a string of 2 or more characters into the textfield, it'll perform validation on the very last character entered, but ignore all previously entered characters. This can lead to inserting more characters than allowed into the textfield.Below I'll post more detail and a solution (out of space here).
Dave Gallagher
+2  A: 

In the above example where I commented, this is bad:

// Don't use:
- (BOOL)isPartialStringValid:(NSString *)partialString
            newEditingString:(NSString **)newString
            errorDescription:(NSString **)error
{
    if ((int)[partialString length] > maxLength)
    {
        *newString = nil;
        return NO;
    }
}

Use this (or something like it) instead:

// Good to use:
- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
       proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
              originalString:(NSString *)origString
       originalSelectedRange:(NSRange)origSelRange
            errorDescription:(NSString **)error
{
    int size = [*partialStringPtr length];
    if ( size > maxLength )
    {
        return NO;
    }
    return YES;
}

Both are NSFormatter methods. The first one has an issue. Say you limit text-entry to 10 characters. If you type characters in one-by-one into an NSTextField, it'll work fine and prevent users from going beyond 10 characters.

However, if a user was to paste a string of, say, 25 characters into the Text Field, what'll happen is something like this:

1) User will paste into TextField

2) TextField will accept the string of characters

3) TextField will apply the formatter to the "last" character in the 25-length string

4) Formatter does stuff to the "last" character in the 25-length string, ignoring the rest

5) TextField will end up with 25 characters in it, even though it's limited to 10.

This is because, I believe, the first method only applies to the "very last character" typed into an NSTextField. The second method shown above applies to "all characters" typed into the NSTextField. So it's immune to the "paste" exploit.

I discovered this just now trying to break my application, and am not an expert on NSFormatter, so please correct me if I'm wrong. And very much thanks to you carlosb for posting that example. It helped a LOT! :)

Dave Gallagher
The user does not even need to paste. A user-defined custom key binding (see http://www.hcs.harvard.edu/~jrus/site/cocoa-text.html for details) can insert any string, and a single code point that is outside the Basic Multilingual Plane will be multiple “characters” in Cocoa's two-byte (UTF-16) sense.
Peter Hosey
Thanks for that awesome article Peter!
Dave Gallagher