NSTokenField does not check token on blur - objective-c

I have an NSTokenField which allows the user to select contacts (Just like in Mail.app). So the NSTextField is bound to an array in my model.recipient instance variable.
The user can now select an entry from the auto completion list e.g. Joe Bloggs: joe#blogs.com and as soon as he hits Enter the token (Joe Bloggs) is displayed and model.recipients now contains a BBContact entry.
Now if the user starts to type some keys (so the suggestions are shown) and then hits Tab instead of Enter the token with the value of the completion text (Joe Bloggs: joe#bloggs.com) is created and the NSTokenFieldDelegate methods did not get called, so that I could respond to this event. The model.recipient entry now contains an NSString instead of a BBContact entry.
Curiously the delegate method tokenField:shouldAddObjects:atIndex: does not get called, which is what I would expect when the user tabs out of the token field.

Pressing tab calls isValidObject on the delegate so return NO for NSTokenField in it however you want to return YES if there are no alphanumeric characters in it otherwise the user won't be able to focus away from the field (the string contains invisible unicode characters based on how many tokens exist)
The less fragile implementation i could come up with is:
- (BOOL)control:(NSControl *)control isValidObject:(id)token
{
if ([control isKindOfClass:[NSTokenField class]] && [token isKindOfClass:[NSString class]])
{
if ([token rangeOfCharacterFromSet:[NSCharacterSet alphanumericCharacterSet]].location == NSNotFound) return YES;
return NO;
}
return YES;
}

I was able to solve the problem using #valexa's suggestions. In case of a blur with TAB I have to go through all entries and look up my contact-objects for any strings.
- (BOOL)control:(NSControl *)control isValidObject:(id)token{
if ([control isKindOfClass:[NSTokenField class]] && [token isKindOfClass:[NSString class]])
{
NSTokenField *tf = (NSTokenField *)control;
if ([token rangeOfCharacterFromSet:[NSCharacterSet alphanumericCharacterSet]].location == NSNotFound){
return YES;
} else {
// We get here if the user Tabs away with an entry "pre-selected"
NSMutableArray *set = #[].mutableCopy;
for(NSObject *entry in tf.objectValue){
GSContact *c;
if([entry isKindOfClass:GSContact.class]){
c = (GSContact *)entry;
}
if([entry isKindOfClass:NSString.class]){
NSString *number = [[(NSString *)entry stringByReplacingOccurrencesOfString:#">" withString:#""]
componentsSeparatedByString:#"<"][1];
c = [self findContactByNumber:number];
}
if(c) [set addObject:c];
}
[control setObjectValue:set];
}
}
return YES;
}

This could be because the "enter" key might send the event of the token field to it's action where the "tab" key just adds text to it. You could try to set the -isContinuous property to YES and see if you get the desired results.

Related

Prevent Users from Entering Multiple Decimals In A String

I am doing a button calculator app (still) in which users press a button, I store a value into a string to display in a text box, and then convert into floats when the user enters the = button.
Works great for the most part, except I cannot figure out how to prevent users from entering more than one decimal in a single string. I thought about passing it to a BOOL method, but none of my class materials cover methods at all.
I looked at this Stack overflow question, but trying to amend the code for my own just resulted in a whole bunch of errors. Does anyone have any advice?
-(IBAction) decimal
{
NSString *decVal =#".";
NSRange range = [decVal rangeOfString:#"."];
if (range.location==NSNotFound)
{
display.text = [display.text stringByAppendingString:decVal];
}
else
{
NSLog(#"You can't enter more than one decimal");
}
}
-(IBAction) decimal
{
static NSString *decVal =#".";
NSRange range = [display.text rangeOfString:decVal];
if (NSNotFound == range.location)
{
display.text = [display.text stringByAppendingString:decVal];
}
else
{
NSLog(#"You can't enter more than one decimal");
}
}
You have missed in one string. [decVal rangeOfString:#"."] will always return range {0,1}.
One way to handle this would be to set the delegate of the text field/view and then implement shouldChangeCharactersInRange in the delegate.
For example, you could do something like this:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
BOOL hasPeriod = ([textField.text rangeOfString:#"."].location != NSNotFound);
BOOL removingPeriod = ([[textField.text substringWithRange:range]
rangeOfString:#"."].location != NSNotFound);
BOOL addingPeriod = ([string rangeOfString:#"."].location != NSNotFound);
return (hasPeriod && addingPeriod && !removingPeriod) ? NO : YES;
}
This won't throw an error. It just won't allow a second period. You could easily add in the error message, though.

How can I force NSTextField to only allow numbers?

In Interface Builder I’ve created a textfield and stuck an NSNumberFormatter into the cell. However, the user can still type text into the textfield. Is there any way to restrict the input to numbers only using interface builder? I thought that was the point of the NSNumberFormatter.
Every formatter has this method:
- (BOOL) getObjectValue: (id*) object forString: (NSString*) string
errorDescription: (NSError**) error;
This is valid for every formatter.
If the string is not valid it returns false and the object passed as argument (dereferenced) will be nil.
So instead of dropping a number formatter to the text field, use your own formatter as instance variable.Observe the text field implementing the delegate protocol so that at every change of the text you can be notified.
Then invoke this method:
NSNumber* number;
BOOL success=[formatter getObjectValue: &number forString: [myTextField stringValue] errorDescription:nil];
At this point if success is false (or check if number is nil), there is an invalid string in the text field, so do the action that is more appropriate for you (maybe delete the entire string, or display 0.0 as value).
There is also another method:
- (BOOL) isPartialStringValid : (NSString*) partialString: (NSString*) partialString
errorDescription: (NSString*) error;
This way you can know if a partial string is valid.For example with the scientific notation "1e" is not valid, but is partially valid because the user may add 1 and it will become "1e1".
Create an NSNumberFormatter subclass and put this in the implementation. In your code, set the YKKNumberFormatter as the formatter for the NSTextField/UITextField.
#implementation YKKNumberFormatter
- (BOOL)isPartialStringValid:(NSString *)partialString newEditingString:(NSString **)newString errorDescription:(NSString **)error {
// Make sure we clear newString and error to ensure old values aren't being used
if (newString) { *newString = nil;}
if (error) {*error = nil;}
static NSCharacterSet *nonDecimalCharacters = nil;
if (nonDecimalCharacters == nil) {
nonDecimalCharacters = [[[NSCharacterSet decimalDigitCharacterSet] invertedSet] retain];
}
if ([partialString length] == 0) {
return YES; // The empty string is okay (the user might just be deleting everything and starting over)
} else if ([partialString rangeOfCharacterFromSet:nonDecimalCharacters].location != NSNotFound) {
return NO; // Non-decimal characters aren't cool!
}
return YES;
}
#end

Dynamically created textfield validation

I'm trying to validate dynamically created text fields. The total number of textfields may vary.
The idea is to populate the empty fields with string like player 1, player 2 etc.. Here is what I try
-(IBAction)validateTextFields:sender
{
self.howManyPlayers = 3;
int emptyFieldCounter = 1;
NSMutableArray *playersNames = [NSMutableArray arrayWithCapacity:self.howManyPlayers];
while (self.howManyPlayers > 1)
{
self.howManyPlayers--;
UITextField *tmp = (UITextField *) [self.view viewWithTag:self.howManyPlayers];
if (tmp.text == nil)
{
[tmp setText:[NSString stringWithFormat:#"Player %d", emptyFieldCounter]];
emptyFieldCounter++;
}
[playersNames addObject:tmp.text];
}
}
The problems is that if I touch the button which invoke validateTextFields method. The first and the second textfield are populated with text Player 1 and Player 2, but the third field is not populated.
I notice also that if I type a text let's say in the second field touch the button then remove the text and again touch the button that field is not populated with text Player X.
How to make all that things to work correctly ?
change your code for two lines like this:
while (self.howManyPlayers >= 1) //edited line
{
UITextField *tmp = (UITextField *) [self.view viewWithTag:self.howManyPlayers];
if (tmp.text == nil)
{
[tmp setText:[NSString stringWithFormat:#"Player %d", emptyFieldCounter]];
emptyFieldCounter++;
}
[playersNames addObject:tmp.text];
self.howManyPlayers--; // moved line
}
I forgot ur second question, so edited my answer.
For that try with this. Change if (tmp.text == nil) with if (tmp.text == nil || [tmp.txt isEqualToString:#""])
The reason only two fields are populated is that you are only going through the while loop twice. It should be
while (self.howManyPlayers >= 1)
You should also move the decrement to the end of your while loop
while (self.howManyPlayers >= 1)
{
// other code here
self.howManyPlayers--;
}
For the second part of your question, I think when you delete the text from the control, it stops being nil and now becomes an empty string. So you need to check for an empty string as well as nil in your code.
if (tmp.text == nil || [tmp.txt isEqualToString:#""])

Make portion of UITextView undeletable

I have a UITextView and need to make a specific portion un-deletable. Its the first 10 characters of the views text.
I just want it so that if the user is tapping the delete key on the keyboard it simply stops when it reaches say the 10th character in.
Edit
Let me go into a bit more detail.
Let's say the prefix is '123456789:'. I want to be able to type anywhere after this prefix, it can't be editable at all though, so '123456789:' shouldn't not be altered at all. Fichek's answer does this perfectly, however the prefix isn't always there, so how can I detect when it isn't in the textview? I thought the if statement did this but it seems not to.
You can use the delegate method textView:shouldChangeTextInRange:replacementText: To tell the text view whether to accept the delete or not.
As the documentation says:
range : The current selection range. If the length of the range is 0, range reflects the current insertion point. If the user presses the Delete key, the length of the range is 1 and an empty string object replaces that single character.
Edit
Here is an implementation where the user can't delete the the first ten characters. But he will be able to insert characters there.
- (BOOL)textView:(UITextView *)textView shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (range.length==1 && string.length == 0) {
// Deleting text
if (range.location <= 9) {
return NO;
}
}
return YES;
}
Here is an implementation where he can't modify the first ten characters at all.
- (BOOL)textView:(UITextView *)textView shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (range.location <= 9) {
return NO;
}
return YES;
}
sch's last edit makes a decent answer, but I want to offer a slightly more flexible approach.
You have to keep in mind the copy/paste system. User might select all the text in text field and try to paste in the entire value which might be perfectly acceptable, but if (range.location <= 9) { return NO; } will reject it. The way I'd do it is put together a string that would be a result of successful edit and then check if that string would start with your desired prefix.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *resultString = [textField.text stringByReplacingCharactersInRange:range withString:string];
NSLog(#"resulting string would be: %#", resultString);
NSString *prefixString = #"blabla";
NSRange prefixStringRange = [resultString rangeOfString:prefixString];
if (prefixStringRange.location == 0) {
// prefix found at the beginning of result string
return YES;
}
return NO;
}
Edit: if you want to check if the current string in text field starts with the prefix, you can use rangeOfString: the same way:
NSRange prefixRange = [textField.text rangeOfString:prefixString];
if (prefixRange.location == 0) {
// prefix found at the beginning of text field
}
for a complete solution you need to handle several cases, including the cut and paste operations that may start in the uneditable part and extend into the part which the user can edit. I added a variable to control whether or not an operation that includes the uneditable part but extends into the editable part, is valid or not. If valid, the range is adjusted to only affect the editable part.
// if a nil is returned, the change is NOT allowed
- (NSString *)allowChangesToTextView:(UITextView *)textView inRange:(NSRange)changeRange withReplacementText:(NSString *)text
immutableUpTo:(NSInteger)lastReadOnlyChar adjustRangeForEdits:(BOOL)adjustRangeForEdits;
{
NSString *resultString = #"";
NSString *currentText = textView.text;
NSInteger textLength = [currentText length];
// if trying to edit the first part, possibly prevent it.
if (changeRange.location <= lastReadOnlyChar)
{
// handle typing or backspace in protected range.
if (changeRange.length <= 1)
{
return nil;
}
// handle all edits solely in protected range
if ( (changeRange.location + changeRange.length) <= lastReadOnlyChar)
{
return nil;
}
// if the user wants to completely prevent edits that extend into the
// read only substring, return no
if (!adjustRangeForEdits)
{
return nil;
}
// the range includes read only part but extends into editable part.
// adjust the range so that it does not include the read only portion.
NSInteger prevLastChar = changeRange.location + changeRange.length - 1;
NSRange newRange = NSMakeRange(lastReadOnlyChar + 1, prevLastChar - (lastReadOnlyChar + 1) + 1);
resultString = [textView.text stringByReplacingCharactersInRange:newRange withString:text];
return resultString;
}
// the range does not include the immutable part. Make the change and return the string
resultString = [currentText stringByReplacingCharactersInRange:changeRange withString:text];
return resultString;
}
and this is how it gets called from the text view delegate method:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
// did the user press enter?
if ([text isEqualToString:#"\n"])
{
[textView resignFirstResponder];
return NO;
}
NSInteger endOfReadOnlyText = [self.spotTextLastSet length] - 1;
NSString *newText = [self allowChangesToTextView:textView inRange:range withReplacementText:text
immutableUpTo:endOfReadOnlyText adjustRangeForEdits:YES];
if (newText == nil)
{
// do not allow!
[TipScreen showTipTitle:#"Information" message:#"The first part of the text is not editable. Please add your comments at the end."
ForScreen:#"editWarning"];
return NO;
}
// lets handle the edits ourselves since we got the result string.
textView.scrollEnabled = NO;
textView.text = newText;
// move the cursor to change range start + length of replacement text
NSInteger newCursorPos = range.location + [text length];
textView.selectedRange = NSMakeRange(newCursorPos, 0);
textView.scrollEnabled = YES;
return NO;
}

determine if a string is a number with NSScanner

i'm trying to find out if my string contains any floatValue, and resign the first responder if it's the case, if it's not, the textfield keyboard should stay on screen.
This code always hides the keyboard, even if it's not a floatValue : do you know how to make it work?
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
NSScanner *scan = [NSScanner scannerWithString:[textField text]];
if ([scan scanFloat:NULL]){
[password resignFirstResponder];
[passwordLength resignFirstResponder];
return YES;
} else {
return NO;
}
}
Also, i haven't tried with loops but this is a beginning, if you have any idea :
BOOL doesStringContain(NSString* string, NSString* string2){
for (int i=0; i<[string length]; i++) {
NSString* chr = [string substringWithRange:NSMakeRange(i, 1)];
for (int j=0; j<[string2 length]; j++){
if([chr isEqualToString:j])
return TRUE;
}
}
return FALSE;
}
Thanks a lot
NSScanner will expect that your float be at the beginning of your string. So to combat that we use setCharactersToBeStripped.
setCharactersToBeStripped will filter out all non-numeric non-period characters from the string so that all you're left with to scan is the number that you're looking for.
NSScanner will match int values (without the .) as well as it will equate an int 123 to 123.00000.
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
NSScanner *scan = [NSScanner scannerWithString:[textField text]];
[scan setCharactersToBeSkipped:[[NSCharacterSet characterSetWithCharactersInString:#"1234567890."] invertedSet]];
float f;
if ([scan scanFloat:&f]){
NSLog(#"Scanned a float %f",f);
[textField resignFirstResponder];
return YES;
} else {
NSLog(#"Did not scan a float");
return NO;
}
}
If you want to check if there are non-numeric characters vs only numerics then try :
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
NSCharacterSet *withFloats = [NSCharacterSet characterSetWithCharactersInString:#"0123456789."];
NSCharacterSet *withoutFloats = [NSCharacterSet decimalDigitCharacterSet];
// Change withFloats <-> withoutFloats depending on your need
NSString *newString = [[textField.text componentsSeparatedByCharactersInSet:withFloats] componentsJoinedByString:#""];
NSLog(#"newString %#", newString);
if ([newString isEqualToString:#""]){
NSLog(#"Scanned a numeric");
[textField resignFirstResponder];
return YES;
} else {
NSLog(#"Did not scan a numeric");
return NO;
}
}
You are implementing the wrong delegate method. textFieldShouldReturn: is called when the user hits the return key. (Not when control is trying to "return from" the field, in case that's what you were thinking.)
You should implement textFieldShouldEndEditing: instead. That is where you can (try to) stop the text field from losing first responder status, and keep the keyboard up. Just check the text like you are doing, and return YES or NO (let the system update first responder status).
If you want the return key to dismiss the keyboard if input is valid, you should call endEditing: there. Like this:
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
return [textField endEditing:NO];
}
The NO parameter means don't force it, basically allowing your textFieldShouldEndEditing: code to check the text first. (You should always call endEditing: if you can, rather than resignFirstResponder.)
Note that, for various reasons, the text field might be forced to give up first responder status anyway, so even if you're validating input in this way, be prepared to validate it again before you save it to disk or send it over the network or whatever you want to do with it.