8

The problem of limiting strings that are directly entered into a UITextView or UITextField has been addressed on SO before:

However now with OS 3.0 copy-and-paste becomes an issue, as the solutions in the above SO questions don’t prevent pasting additional characters (i.e. you cannot type more than 10 characters into a field that is configured with the above solutions but you can easily paste 100 characters into the same field).

Is there a means of preventing directly entered string and pasted string overflow?

Community
  • 1
  • 1
Kevin L.
  • 4,548
  • 7
  • 39
  • 54
  • possible duplicate of [iPhone SDK: Set Max Character length TextField](http://stackoverflow.com/questions/433337/iphone-sdk-set-max-character-length-textfield) – JosephH Mar 05 '12 at 11:00

8 Answers8

10

I was able to restrict entered and pasted text by conforming to the textViewDidChange: method within the UITextViewDelegate protocol.

- (void)textViewDidChange:(UITextView *)textView
{
    if (textView.text.length >= 10)
    {
        textView.text = [textView.text substringToIndex:10];
    }
}

But I still consider this kind of an ugly hack, and it seems Apple should have provided some kind of "maxLength" property of UITextFields and UITextViews.

If anyone is aware of a better solution, please do tell.

Kevin L.
  • 4,548
  • 7
  • 39
  • 54
7

In my experience just implementing the delegate method:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string

works with pasting. The entire pasted string comes across in the replacementString: argument. Just check it's length, and if it's longer than your max length, then just return NO from this delegate method. This causes nothing to be pasted. Alternatively you could substring it like the earlier answer suggested, but this works to prevent the paste at all if it's too long, if that's what you want.

Rob
  • 1,364
  • 11
  • 17
6

Changing the text after it's inserted in textViewDidChange: causes the app to crash if the user presses 'Undo' after the paste.

I played around for quite a bit and was able to get a working solution. Basically the logic is, do not allow the paste if the total length is greater than the max characters, detect the amount that is overflown and insert only the partial string.

Using this solution your pasteboard and undo manager will work as expected.

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    NSInteger newLength = textView.text.length - range.length + text.length;

    if (newLength > MAX_LENGTH) {
        NSInteger overflow = newLength - MAX_LENGTH;

        dispatch_async(dispatch_get_main_queue(), ^{
            UITextPosition *start = [textView positionFromPosition:nil offset:range.location];
            UITextPosition *end = [textView positionFromPosition:nil offset:NSMaxRange(range)];
            UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
            [textView replaceRange:textRange withText:[text substringToIndex:text.length - overflow]];
        });
        return NO;
    }
    return YES;
}
Brad G
  • 2,528
  • 1
  • 22
  • 23
  • This can be done without the dispatch_async. dispatch_async will cause a crash on iOS8 with the new type ahead feature. I'll rework the code soon. – Brad G Nov 18 '14 at 03:47
  • 1
    Passing nil as a first argument to `positionFromPosition:offset:` is not allowed. – bompf Aug 13 '18 at 15:36
  • This create a crash if you hit redo twice. Any change to prevent this crash?Thanks – Besat Sep 11 '18 at 13:07
2

This code won't let user to input more characters than maxCharacters. Paste command will do nothing, if pasted text will exceed this limit.

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    let newText = (textView.text as NSString).replacingCharacters(in: range, with: text)
    return newText.count <= maxCharacters;
}
alekseevpg
  • 533
  • 4
  • 10
0

One of the answers in the first question you linked above to should work, namely using something like

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(limitTextField:) name:@"UITextFieldTextDidChangeNotification" object:myTextField];

to watch for changes to the text in the UITextField and shorten it when appropriate.

David Maymudes
  • 5,664
  • 31
  • 34
  • 1
    In this special case, conforming to the UITextViewDelegate (or UITextFieldDelegate) is preferable to rolling your own notification (and easier to debug). And I should probably post how I actually pulled this issue off... – Kevin L. Jul 17 '09 at 19:05
0

Also, string length as in '[string length]' is one thing, but one often needs to truncate to a byte count in a certain encoding. I needed to truncate typing and pasting into a UITextView to a max UTF8 count, here's how I did it. (Doing something similar for UITextField is an exercise to the reader.)

NSString+TruncateUTF8.h

#import <Foundation/Foundation.h>
@interface NSString (TruncateUTF8)
- (NSString *)stringTruncatedToMaxUTF8ByteCount:(NSUInteger)maxCount;
@end

NSString+TruncateUTF8.m

#import "NSString+TruncateUTF8.h"
@implementation NSString (TruncateUTF8)
- (NSString *)stringTruncatedToMaxUTF8ByteCount:(NSUInteger)maxCount {
  NSRange truncatedRange = (NSRange){0, MIN(maxCount, self.length)};
  NSInteger byteCount;

  // subtract from this range to account for the difference between NSString's
  // length and the string byte count in utf8 encoding
  do {
    NSString *truncatedText = [self substringWithRange:truncatedRange];
    byteCount = [truncatedText lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
    if (byteCount > maxCount) {
      // what do we subtract from the length to account for this excess count?
      // not the count itself, because the length isn't in bytes but utf16 units
      // one of which might correspond to 4 utf8 bytes (i think)
      NSUInteger excess = byteCount - maxCount;
      truncatedRange.length -= ceil(excess / 4.0);
      continue;
    }
  } while (byteCount > maxCount);

  // subtract more from this range so it ends at a grapheme cluster boundary
  for (; truncatedRange.length > 0; truncatedRange.length -= 1) {
    NSRange revisedRange = [self rangeOfComposedCharacterSequencesForRange:truncatedRange];
    if (revisedRange.length == truncatedRange.length)
      break;
  }

  return (truncatedRange.length < self.length) ? [self substringWithRange:truncatedRange] : self;
}
@end

// tested using:
//    NSString *utf8TestString = @"Hello world, Καλημέρα κόσμε, コンニチハ ∀x∈ℝ ıntəˈnæʃənəl ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈ STARGΛ̊TE γνωρίζω გთხოვთ Зарегистрируйтесь ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช ሰማይ አይታረስ ንጉሥ አይከሰስ። ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ⡌⠁⠧⠑ ⠼⠁⠒  ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌ ░░▒▒▓▓██ ▁▂▃▄▅▆▇█";
//    NSString *truncatedString;
//    NSUInteger byteCount = [utf8TestString lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
//    NSLog(@"length %d: %p %@", (int)byteCount, utf8TestString, utf8TestString);
//    for (; byteCount > 0; --byteCount) {
//        truncatedString = [utf8TestString stringTruncatedToMaxUTF8ByteCount:byteCount];
//        NSLog(@"truncate to length %d: %p %@ (%d)", (int)byteCount, truncatedString, truncatedString, (int)[truncatedString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
//    }

MyViewController.m

#import "NSString+TruncateUTF8.h"
...
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)replacementText
{
  NSMutableString *newText = textView.text.mutableCopy;
  [newText replaceCharactersInRange:range withString:replacementText];

  // if making string larger then potentially reject
  NSUInteger replacementTextLength = replacementText.length;
  if (self.maxByteCount > 0 && replacementTextLength > range.length) {
    // reject if too long and adding just 1 character
    if (replacementTextLength == 1 && [newText lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > self.maxByteCount) {
      return NO;
    }

    // if adding multiple charaters, ie. pasting, don't reject altogether but instead return YES
    // to accept and truncate immediately after, see http://stackoverflow.com/a/23155325/592739
    if (replacementTextLength > 1) {
      NSString *truncatedText = [newText stringTruncatedToMaxUTF8ByteCount:self.maxByteCount]; // returns same string if truncation needed
      if (truncatedText != newText) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0LL), dispatch_get_main_queue(), ^{
          UITextPosition *replaceStart = [textView positionFromPosition:textView.beginningOfDocument offset:range.location];
          UITextRange *textRange = [textView textRangeFromPosition:replaceStart toPosition:textView.endOfDocument];
          [textView replaceRange:textRange withText:[truncatedText substringFromIndex:range.location]];

          self.rowDescriptor.value = (truncatedText.length > 0) ? truncatedText : nil;
        });
      }
    }
  }

  [self updatedFieldWithString:(newText.length > 0) ? newText : nil]; // my method
  return YES;
}
Pierre Houston
  • 1,631
  • 20
  • 33
  • thanks man! this snippet helped me to figure out the correct way of replacing text in a textfield! I made a swift 3 version: https://gist.github.com/Blackjacx/2198d86442ec9b9b05c0801f4e392047 – blackjacx Apr 11 '17 at 14:47
0

You can know the pasted string if you check for string.length in shouldChangeCharactersIn range: delegate method

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if string.length > 1 {
        //pasted string
        // do you stuff like trim
    } else {
        //typed string
    }
    return true
}
Rugmangathan
  • 3,186
  • 6
  • 33
  • 44
  • can you implement it a little bit clearer., like I want the textField always take the firsr 6 letters? – Thao Tran May 02 '21 at 06:33
  • @ThaoTran For taking first six digits you can use `string.prefix(6)` to take first six digits of pasted string inside the `string.length > 1` condition. – Rugmangathan May 31 '21 at 14:01
-1
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{

  if(string.length>10){
    return NO;
  }
  return YES;
}
Kjuly
  • 34,476
  • 22
  • 104
  • 118
SachinVsSachin
  • 6,401
  • 3
  • 33
  • 39