How can I force NSTextField to only allow numbers? - objective-c

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

Related

Building 3 methods in Objective C

I'm having trouble with a school problem in Objective C. I need to build 3 methods. The first method tells you if someone is in a line. If nobody is in the line it tells you nobody is in the line otherwise it tells you who is in the line and it lists the names on a new line.
The second method adds names to the line.
The third method removes a name from the line and tells you who was removed.
First method:
-(NSString*)stringWithDeliLine:(NSArray*) deliLine{
NSString *empty = #"The line is currently empty.";
//Some kind of formatted string
if(deliLine == nil || [deliLine count] == 0)
{
empty;
}
else
{
//formatted string
}
//not sure how to return either empty or formatted string
}
Second Method:
-(void)addName:toDeliLine:(NSString*)name:(NSMutableArray*)deliLine{
[deliLine addObject:name];
}
The third method I was going to use removeObject but the instructions said not to use it so I have no idea where to start.I have the signature I think.
-(NSString*)serveNextCustomerInDeliLine:(NSMutableArray*)deliLine{
return nil;
}
For the first method I'm not sure why my literal string won't work in the if statement. I thought I was saying look at the array if nothing is in the array then it's the first object and show the string literal. else show some kinda of formatted string. I've tried all kinds of strings but none seem to be working so that's why I have the comment formatted string. If someone could give me a hint that would be great. I don't need the answer just a clue on what to think about. This is long post sorry.
A possible implementation can be the following. Please note that I have not testes edge cases and I wrote the code without Xcode support
#import <Foundation/Foundation.h>
#interface Line : NSObject
- (NSString*)printLine;
- (void)addCustomer:(NSString*)customer;
- (NSString*)removeCustomer:(NSString*)customer;
#end
#import "Line.h"
#interface Line ()
#property (nonatomic, strong, readwrite) NSMutableArray<NSString*> *customers;
#end
#implementation Line
- (instancetype)init {
self = [super init];
if (self) {
_customers = [NSMutableArray array];
}
return self;
}
- (NSString*)printLine {
NSUInteger count = self.customers.count;
if(count == 0) {
return #"Empty";
}
NSMutableString *descr = [NSMutableString string];
for (NSString *customer in self.customers) {
[descr appendString:[NSString stringWithFormat:#"%# ", customer]];
}
return [descr copy];
}
- (void)addCustomer:(NSString*)customer {
[self.customers addObject:customer];
}
- (NSString*)removeCustomer:(NSString*)customer {
NSUInteger index = [self.customers indexOfObject:customer];
if(index == NSNotFound) {
return [NSString stringWithFormat:#"%# not removed", customer];
}
NSString *removedCustomer = [self.customers objectAtIndex:index];
[self.customers removeObjectAtIndex:index];
return removedCustomer;
}
#end
Usage:
Line *line = [[Line alloc] init];
[line addCustomer:#"customer"];
NSLog(#"%#", [line printLine]);
NSLog(#"%#", [line removeCustomer:#"customer"]);
NSLog(#"%#", [line printLine]);
Edit:
I've updated my answer, passing the array as a parameter is not necessary, just initialize deliLine as a mutable array property.
For you first method, you could do the following,
- (NSString *)deliLineContents {
NSString *empty = #"The line is currently empty.";
NSMutableString *namesInQueue = [[NSMutableString alloc] init];
if(self.deliLine == nil || [self.deliLine count] == 0) {
return empty;
} else {
// Loop through your array and return a string of all the names
for (NSString *string in self.deliLine ) {
[namesInQueue appendString:string];
}
}
return [NSString stringWithString:namesInQueue];
For your second method, you're already pretty much there, maybe look up how to construct method signatures.
- (void)addNameToDeliLine:(NSString*)name {
[self.deliLine addObject:name];
}
For your third method, not sure if this meets your requirement, if not let me know.
- (NSString *)customerRemovedFromLine {
// I've making an assumption that you want to remove the first customer
NSString *servedCustomer = [self.deliLine objectAtIndex:0];
[self.deliLine removeObjectAtIndex:0];
return servedCustomer;
}
You probably don't need to pass deliLine around, just create it as a property and access it with self.deliLine. Anyway hope this helps, good luck.

Cocoa binding: NSTextField with empty string for zero value

I have NSTextField with placeholder. And it's binded to some integer property. So I want to display empty text in the field (with placeholder shown) when binded integer is zero.
Is it possible to do it?
(Update)
I discovered that this can be done through NSNumberFormatter - it has —(void) setZeroSymbol: (NSString*) string method. Not tried yet this in practice...
You could use an NSValueTransformer.
(Just in case)Create a new class, subclass from NSValueTransformer. In the implementation, add something like this:
+(Class)transformedValueClass {
return [NSString class];
}
-(id)transformedValue:(id)value {
if (value == nil) {
return nil;
} else {
if ([value integerValue] == 0) {
return #"";
} else {
return [NSString stringWithFormat:#"%d", [value stringValue]];
}
}
}
In Interface Builder, select your field, go to the bindings tab, and in the Value Transformer drop down, either select or type in your class name you made. This should prevent you from having to worry about modifying it elsewhere. I'm not 100% positive about it showing the placeholder (I don't have a Mac available right now).
EDIT:
I can confirm that this does indeed work. Here is a link to a github project I made to show how to use it: https://github.com/macandyp/ZeroTransformer
check the integer value before binding, if you are binding at runtime. Try
int i;
if (i == 0)
{
txt.placeholder = #"text";
}
else
{
[txt setStringValue:[NSString stringWithFormat:#"%d",i]];
}
You can not do conditional binding.
You need to create another property that will hold the value based on condition and use that property and bind to textfield.
I am using bindedString and bindedInteger. bindedString is bound to text field.
Whenever some action is performed it is updated.
- (id)init{
self = [super init];
if (self) {
self.bindedString=#"place holder string";
}
return self;
}
- (IBAction)button:(id)sender {
if (self.bindedInteger==0) {
self.bindedString=#"place holder string";
}
else{
self.bindedString=[NSString stringWithFormat:#"%ld",self.bindedInteger];
}
}

How could I get the property name of the current UITextField a user is in as a string?

I am using the following method:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string
Before, in order to find the current text field I was in, I was able to write something like this:
if (textField == self.textPlaceID)
{
//Do something
}
Is there a way to grab the name of my textField as a string? I am not asking to take what the user has typed in that textField, I'd like to be able to just get the property name of if. I need it to then concatenate it with some other strings. I am working on making a dynamic method call.
UITextFields do not have "names". You can give your text field a tag and use that.
Also, note that (pointer == pointer) will only return true if you are referencing the same objects, not equivalent values.
Here is how to use the tag: in Interface Builder, give each text field a tag, or if you create your text fields programmatically, set textField.tag = someInt; I usually use macros to make the code more readable:
#define kNameTextField 2
#define kAddressTextField 3
...
if (textField.tag == kNameTextField) ...
With lots of fields like that, I prefer enums:
typedef enum {
kNameTextField = 2,
kAddressTextField,
kPhoneTextField // etc
} Fields;
You can get name of property (in your case textField property) using this code:
-(NSString *)propertyName:(id)property {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
if ((object_getIvar(self, thisIvar) == property)) {
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}
Remember to import:
#import <objc/runtime.h>
Just call:
NSLog(#"name = %#", [self propertyName:self.textField]);
source
Primarily assign unique tags to each of the textFields (eg 1,2,3.....) avoid 0 for a textField tag
-(void)viewDidLoad{
NSDictionary *dictionary ; //declare it in .h interface
dictionary = [NSDictionary dictionaryWithObjectsAndKeys:#"textPlaceID",[NSNumber numberWithInt:1], #"textPlaceID2",[NSNumber numberWithInt:2],nil]; // add textField as object and tag as keys
}
further..
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string{
NSString *textFieldPropertyName = [dictionary objectForKey:[NSNumber numberWithInt:textField.tag]];
}

NSFormatter for BOOL

I have set up my simple Xcode project with a table that is binded to an array controller. It works fine if the array controller is full of entities with a string attribute. However I want to change the attribute to a BOOL and have the table show the string "true" or "false" based on the BOOL.
I have overrided the following two methods from NSFormatter:
-(NSString*) stringForObjectValue:(id)object {
//what is the object?
NSLog(#"object is: %#", object);
if(![object isKindOfClass: [ NSString class ] ] ) {
return nil;
}
//i'm tired....just output hello in the table!!
NSString *returnStr = [[NSString alloc] initWithFormat:#"hello"];
return returnStr;
}
-(BOOL)getObjectValue: (id*)object forString:string errorDescription:(NSString**)error {
if( object ) {
return YES;
}
return NO;
}
So the table gets populated with "hello" if the attribute is a string however if I switch it to a boolean, then the table gets populated with lots of blank spaces.
I don't know if this helps but on the line where I'm outputting the object, it outputs __NSCFString if the attribute is a string and "Text Cell" if I switch the attribute to a boolean. This is something else I don't understand.
Ok, it's not 100% clear what you're trying to do from the code, but first things first - BOOL is not an object, it's basically 0 or 1, so to place BOOL values into an array, you're probably best off using NSNumber:
NSNumber *boolValue = [NSNumber numberWithBool:YES];
and placing these into your array. Now you want to change your method:
-(NSString*) stringForObjectValue:(id)object {
NSNumber *number = (NSNumber *)object;
if ([number boolValue] == YES)
return #"true";
else
return #"false";
}
There's a few things here - for example, you want to avoid passing around id references if you can (if you know all your objects in the NSArray are NSNumber, you shouldn't need to).

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;
}