Как ограничить длину текста NSTextField и оставить его всегда в верхнем регистре?
Нужно иметь NSTextField с текстовым ограничением максимум 4 символа и показывать всегда в верхнем регистре, но не могу найти хороший способ добиться этого. Я пытался сделать это с помощью привязки с методом проверки, но проверка вызывается только тогда, когда элемент управления теряет первого респондента, и это не хорошо.
Временно я заставил это работать, наблюдая уведомление NSControlTextDidChangeNotification на текстовом поле и заставляя это вызывать метод:
- (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]];
}
}
Но это, безусловно, не лучший способ сделать это. Любое лучшее предложение?
7 ответов
Я сделал так, как предложил Грэм Ли, и он отлично работает, вот код пользовательского форматера:
ОБНОВЛЕНО: Добавлено исправление, о котором сообщил Дэйв Галлахер. Спасибо!
@interface CustomTextFieldFormatter : NSFormatter {
int maxLength;
}
- (void)setMaximumLength:(int)len;
- (int)maximumLength;
@end
@implementation CustomTextFieldFormatter
- (id)init {
if(self = [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
В приведенном выше примере, где я прокомментировал, это плохо:
// Don't use:
- (BOOL)isPartialStringValid:(NSString *)partialString
newEditingString:(NSString **)newString
errorDescription:(NSString **)error
{
if ((int)[partialString length] > maxLength)
{
*newString = nil;
return NO;
}
}
Используйте это (или что-то подобное) вместо этого:
// 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;
}
Оба являются NSFormatter методами. У первого есть проблема. Допустим, вы ограничиваете ввод текста до 10 символов. Если вы вводите символы один за другим в NSTextField, это будет работать нормально и не позволит пользователям выходить за пределы 10 символов.
Однако, если пользователь вставит строку, скажем, из 25 символов в текстовое поле, произойдет что-то вроде этого:
1) Пользователь вставит в TextField
2) TextField будет принимать строку символов
3) TextField применит форматер к "последнему" символу в строке длиной 25
4) Formatter делает вещи для "последнего" символа в строке длиной 25, игнорируя остальные
5) TextField будет содержать 25 символов, хотя он ограничен 10.
Это потому, что, я полагаю, первый метод применяется только к "самому последнему символу", введенному в NSTextField. Второй метод, показанный выше, применяется ко всем символам, введенным в NSTextField. Так что он невосприимчив к эксплойту "вставить".
Я обнаружил это только сейчас, пытаясь сломать мое приложение, и я не эксперт по NSFormatter, поэтому, пожалуйста, исправьте меня, если я ошибаюсь. И большое спасибо вам, Карлосб, за публикацию этого примера. Это помогло МНОГО!:)
Эта реализация принимает несколько предложений, прокомментированных выше. Примечательно, что он работает правильно с постоянно обновляемыми привязками.
К тому же:
Он правильно выполняет вставку.
Он включает в себя некоторые заметки о том, как эффективно использовать класс в кончике без дополнительных подклассов.
Код:
@interface BPPlainTextFormatter : NSFormatter {
NSInteger _maxLength;
}
/*
Set the maximum string length.
Note that to use this class within a Nib:
1. Add an NSFormatter as a Custom Formatter.
2. In the Identity inspector set the Class to BPPlainTextFormatter
3. In user defined attributes add Key Path: maxLength Type: Number Value: 30
Note that rather than attaching formatter instances to individual cells they
can be positioned in the nib Objects section and referenced by numerous controls.
A name, such as Plain Text Formatter 100, can be used to identify the formatters max length.
*/
@property NSInteger maxLength;
@end
@implementation BPPlainTextFormatter
@synthesize maxLength = _maxLength;
- (id)init
{
if(self = [super init]){
self.maxLength = INT_MAX;
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
// support Nib based initialisation
self = [super initWithCoder:aDecoder];
if (self) {
self.maxLength = INT_MAX;
}
return self;
}
#pragma mark -
#pragma mark Textual Representation of Cell Content
- (NSString *)stringForObjectValue:(id)object
{
NSString *stringValue = nil;
if ([object isKindOfClass:[NSString class]]) {
// A new NSString is perhaps not required here
// but generically a new object would be generated
stringValue = [NSString stringWithString:object];
}
return stringValue;
}
#pragma mark -
#pragma mark Object Equivalent to Textual Representation
- (BOOL)getObjectValue:(id *)object forString:(NSString *)string errorDescription:(NSString **)error
{
BOOL valid = YES;
// Be sure to generate a new object here or binding woe ensues
// when continuously updating bindings are enabled.
*object = [NSString stringWithString:string];
return valid;
}
#pragma mark -
#pragma mark Dynamic Cell Editing
- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
originalString:(NSString *)origString
originalSelectedRange:(NSRange)origSelRange
errorDescription:(NSString **)error
{
BOOL valid = YES;
NSString *proposedString = *partialStringPtr;
if ([proposedString length] > self.maxLength) {
// The original string has been modified by one or more characters (via pasting).
// Either way compute how much of the proposed string can be accommodated.
NSInteger origLength = origString.length;
NSInteger insertLength = self.maxLength - origLength;
// If a range is selected then characters in that range will be removed
// so adjust the insert length accordingly
insertLength += origSelRange.length;
// Get the string components
NSString *prefix = [origString substringToIndex:origSelRange.location];
NSString *suffix = [origString substringFromIndex:origSelRange.location + origSelRange.length];
NSString *insert = [proposedString substringWithRange:NSMakeRange(origSelRange.location, insertLength)];
#ifdef _TRACE
NSLog(@"Original string: %@", origString);
NSLog(@"Original selection location: %u length %u", origSelRange.location, origSelRange.length);
NSLog(@"Proposed string: %@", proposedString);
NSLog(@"Proposed selection location: %u length %u", proposedSelRangePtr->location, proposedSelRangePtr->length);
NSLog(@"Prefix: %@", prefix);
NSLog(@"Suffix: %@", suffix);
NSLog(@"Insert: %@", insert);
#endif
// Assemble the final string
*partialStringPtr = [[NSString stringWithFormat:@"%@%@%@", prefix, insert, suffix] uppercaseString];
// Fix-up the proposed selection range
proposedSelRangePtr->location = origSelRange.location + insertLength;
proposedSelRangePtr->length = 0;
#ifdef _TRACE
NSLog(@"Final string: %@", *partialStringPtr);
NSLog(@"Final selection location: %u length %u", proposedSelRangePtr->location, proposedSelRangePtr->length);
#endif
valid = NO;
}
return valid;
}
@end
Мне нужен был Formatter для преобразования в прописные буквы для Swift 4. Для справки я включил его здесь:
import Foundation
class UppercaseFormatter : Formatter {
override func string(for obj: Any?) -> String? {
if let stringValue = obj as? String {
return stringValue.uppercased()
}
return nil
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
obj?.pointee = string as AnyObject
return true
}
}
Быстрая версия Карлоса Барбозы ответит, если кому-то это нужно.
Пример использования:
myTextField.formatter = CustomTextFieldFormatter(maxLength: 10, isUppercased: true)
class CustomTextFieldFormatter: Formatter {
var maxLength: UInt
var isUppercased: Bool
init(maxLength: UInt, isUppercased: Bool) {
self.maxLength = maxLength
self.isUppercased = isUppercased
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func string(for obj: Any?) -> String? {
return obj as? String
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
obj?.pointee = string as AnyObject
return true
}
override func isPartialStringValid(_ partialStringPtr: AutoreleasingUnsafeMutablePointer<NSString>, proposedSelectedRange proposedSelRangePtr: NSRangePointer?, originalString origString: String, originalSelectedRange origSelRange: NSRange, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
if partialStringPtr.pointee.length > maxLength {
return false
}
if isUppercased && partialStringPtr.pointee != partialStringPtr.pointee.uppercased as NSString {
partialStringPtr.pointee = partialStringPtr.pointee.uppercased as NSString
return false
}
return true
}
override func attributedString(for obj: Any, withDefaultAttributes attrs: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString? {
return nil
}
}
Пользовательский NSFormatter, предложенный Грэмом Ли, - лучший подход.
Простым клуджем было бы установить контроллер представления в качестве делегата текстового поля, а затем просто заблокировать любое редактирование, в котором не прописные буквы или длина которого больше 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;
}