Кроме того, длина строки, как в «[длина строки]», это одно, но в определенной кодировке часто приходится сокращать количество байтов. Мне нужно было урезать ввод и вставку в UITextView до максимального количества UTF8, вот как я это сделал. (Выполнение чего-то подобного для UITextField - это упражнение для читателя.)
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;
}