This one is driving me a little batty and my eyes are glossing over.
I have an app with a navigation controller.
View A has some input fields and a "Continue" button which loads View B
When I tap the "Back" button on the upper left of the navigation controller its resulting in events being fired in an order I'm not expecting/understanding
My tracing reveals ...
View B: viewWillDisappear
View A: viewWillAppear
View B: textFieldShouldEndEditing
EDIT -- more detail/code explaining my previously vague question
Conceptually the following approach has been working fine, and passed several rounds of QA testing.
In summary, I'm using the textFieldShouldEndEditing to validate textfields. If they aren't valid, I retain focus on the field and show them a message of whats wrong. All is good and validations work as the user attempts to go from field to field.
The condition that is problematic with the code below is if someone enters a partial value and then clicks BACK. All of the UITextFields in the entire app Freeze up (don't allow input) and in some cases the app crashes.
The approach I'm attempting which led me to post the initial Question was to create a private: BOOL isDisappearing;
Which I could check in viewWillDisappear (which in most cases fires PRIOR to textFieldShouldEndEditing), and if its YES I would short circuit the problematic code that is firing and freezing the UITextFields/app.
This is working in several views fine, but in 1 case where 'VIEW A:viewWillAppear' event fires before the textFieldShouldEndEditing below (VIEW B) - the isDisappearing gets set to 'NO' somehow and the problematic code is firing in textFieldShouldEndEditing
I hope this helps and you can follow. I find it hard to explain without code – but I've tried to trim it down to just what is relevant. I hope this is appropriate here – I'm pretty new to the community.
Code for VIEW B:
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)rangereplacementString:(NSString *)string
{
// Enforce max lengths
// The return key from the keyboard counts as a character, so we have to exempt it
if (textField.tag == ROUTING_NUMBER_TAG)
{
NSUInteger newLength = [textField.text length] + [string length] - range.length;
return (newLength > 9 && ![string isEqualToString:#"\n"]) ? NO : YES;
}
else if (textField.tag == ACCOUNT_NUMBER_TAG)
{
NSUInteger newLength = [textField.text length] + [string length] - range.length;
return (newLength > 17 && ![string isEqualToString:#"\n"]) ? NO : YES;
}
return YES;
}
// textField validation
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
if (isDisappearing)
return YES;
//run fields through validators and display validation messages.
//IF THEY DON’T PASS VALIDATION IM "HOLDING THEM HOSTAGE" BY KEEPING THE FOCUS ON THE UITEXTFIELD (returning NO)
if (textField.tag == ROUTING_NUMBER_TAG )
{
if ([Utility isValidRoutingNumber:textField.text]== NO)
{
[[iToast makeText:NSLocalizedString(#"Enter valid routing number", #"")] show];
return NO;
}
else
{ //save it
extension.payment.routingNumber = routingNumber.text;
}
}
else if (textField.tag == ACCOUNT_NUMBER_TAG)
{
if ([Utility isValidAccountNumber:textField.text] == NO)
{
[[iToast makeText:NSLocalizedString(#"Enter valid account number", #"")] show];
return NO;
}
else
{ //save it
extension.payment.accountNumber = accountNumber.text;
}
}
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
//I do nothing here except nulling out the 'activefield' var I use to autoscroll the uiscrollview as the user taps around from field to field
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//reset the bool so that when they come back we're back to the 'normal' state and validation will again be checked in textFieldShouldEndEditing
isDisappearing = NO;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
-(void)viewWillDisappear:(BOOL)animated
{
TRC_ENTRY
//set the bool to bypass validations in textFieldShouldEndEditing
isDisappearing = YES;
[super viewWillDisappear:animated];
}
Other than cases where the order of messages is clearly defined or strongly implied by names, you should avoid depending on any particular order. For example, you can reasonably expect -viewWillAppear to be called before -viewDidAppear for any given view, but don't expect one view's -viewWillAppear to be called in any particular order with respect to any message sent to a different view.
If you need help figuring out how to implement a particular feature without depending on order, please ask. But again, unless the order of invocation is documented or blatantly obvious from the method names, don't expect a particular order.
Update: I don't see exactly what's going wrong in the code you added, but perhaps a few suggestions will help:
Is your isDisappearing variable an instance variable of your view controller, a global variable, or what? If it's an instance variable, figure out how it's being changed. If it's a global variable, well... don't do that.
Be sure that you're heeding the warning in the docs to the effect that -textViewShouldEndEditing: is only advisory, and that the view may stop editing no matter what you return.
Try temporarily removing the iToast stuff. If the crash still happens, at least you've eliminated that as a source of problems. If it stops happening, you'll have narrowed your search.
Identify the cause of the crash. (This should really be first on the list.) Crashes don't just happen mysteriously -- there's a reason that it happens. Find that reason, and you're 85% done. Start by examining the stack trace when the crash occurs. If that doesn't provide enough clues, place a breakpoint somewhere before the line that causes the crash and start stepping until you crash. If all else fails, start logging messages to trace execution and monitor your assumptions.
What exactly is your view controller A doing in its -viewWillAppear method? Could that be part of the problem? Could you move that code to, say, -viewDidAppear instead?
Try doing [self.youTextField resignFirstResponder] on view B viewWillDisappear.
This might not be directly relevant to your question, but if you feel you don't fully understand lifetime of views, I suggest take a look here: What is the process of a UIViewController birth (which method follows which)?
It's a great explanation.
Related
I have 2 textFields side by side, countryCodeTextField and cellphoneTextField
On countryCodeTextField. I have an action selectCountry that happens on Edit Did Begin on the countryCodeTextField
- (IBAction)selectCountry:(id)sender {
countryCodeTextField.delegate = self;
[countryCodeTextField resignFirstResponder];
Note that self implements the <UITextFieldDelegate>.
Problem is when user click's cellphone the keyboard is displayed if he clicks on countryCodeTextField the keyboard is never dismissed.
If the person clicks the countryCode first then the keyboard never appears(which is what I want).
Why isn't the keyboard hidden when the user clicks cellphoneTextField first and then countryCodeTextField?
If you don't want the user to be able to edit a particular UITextField, set it to not be enabled.
UITextField *textField = ... // Allocated somehow
textfield.enabled = NO
Or just check the enabled checkbox in Interface Builder. Then the textfield will still be there and you'll be able to update it by configuring the text. But as sort of mentioned in comments, users expect UITextFields to be editable.
Also, why are you setting the delegate in the IBAction callback? I would think you'd be better off doing this in Interface Builder or when you create the UITextField in code.
EDIT:
Ok - so you want users to be able to select the box, but then bring up a custom subview(s) from which they select something which will fill the box.
So set the UITextField delegate when you create it (as mentioned above) and implement the following from the UITextFieldDelegate protocol:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
return NO;
}
to return NO. Note that if you are using the same delegate for both of your UITextFields, you will need to make this method return YES for the other field. For example, something like:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
if (textField == countryTextField)
return NO;
return YES;
}
Hopefully this should stop the keyboard being displayed - and now you have to work out how to fire your own subviews, which I'd suggest doing via an IBAction (touch up or something perhaps). You'll have to test various things out here, but remember you're kinda corrupting the point of UITextField and maybe it'll work and maybe it won't, and maybe it'll break in the next iOS upgrade.
Okay, so first, I think you shouldn't be using a UITextField. I think you should be using a UIButton and have the current value showing as the button's title. However, if you have your heart set on it, I would use our good friend inputView, a property on UITextField, and set that to your custom input view (which I assume is a UIPickerView or similar.)
This has the added bonus of not breaking your app horribly for blind and visually impaired users, something you should probably be aware of before you go messing about with standard behaviour.
In your method :
- (IBAction)textFieldDidBeginEditing: (UITextField *)textField
call this :
[textField becomeFirstResponder];
and apply checks for the two fields i.e., when textField is the countryCodeTextField write :
[textField resignFirstResponder];
and call your method :
[self selectCountry];
In this method display the list of country codes.
So Your code will be :
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
return YES;
}
- (IBAction)textFieldDidBeginEditing: (UITextField *)textField{
[textField becomeFirstResponder];
if (textField == countryCodeTextField){
[textField resignFirstResponder];
[self selectCountry];
}
}
-(IBAction)selectCountry{
//display the list no need to do anything with the textfield.Only set text of TextField as the selected countrycode.
}
I am working this on Xcode 4.3 target iOS5 for iPhone.
I have View B which is segue-ed from View A using simple button action. In View B have text field and I need this text field input validated. The validation rule is simple: if the text field is empty then textFieldShouldEndEditing return NO. The code is:
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
if (![textField.text length]) {
return NO;
}
return YES;
}
This working fine. However, if I have not input anything and 'back' button is pushed (which is pop the view controller) return to View A and I push the button again in View A to navigate to View B, the view B is completely disabled and I can not edit anything in the text field. This is not the case when the validation is not implemented OR always return YES. I also tried if I input something but textFieldShouldEndEditing always return NO;
I trace the code and notice that the textFieldShouldEndEditing is also fired if the controller pop-ed. I think the problem is something to do with thins setting textFieldShouldEndEditing set to YES or NO, but I completely confused.
Please help...
Try checking if the view controller is the top view controller first:
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
if (self == self.navigationController.topViewController)
if (textField == self.confirmationCodeField)
if (textField.text.length < kAuthCodeMinLength)
return NO;
return YES; // default
}
I have noticed, in one of my views in an iPad app I am building the next button on the keyboard goes through all the UITextFields from left to right down the screen.
Is it possible somehow to make it go top to bottom then right, top to bottom?
So say I have to two long columns of text fields, I wan to go top to bottom not left to right, make sense?
Any help appreciated, thanks.
I don't think there is a way through IB, but you can do this way in code. You're not actually tabbing, you'd be using the return key.
Put this in your UITextField's delegate:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
BOOL shouldChangeText = YES;
if ([text isEqualToString:#"\n"]) {
// Find the next entry field
BOOL isLastField = YES;
for (UIView *view in [self entryFields]) {
if (view.tag == (textView.tag + 1)) {
[view becomeFirstResponder];
isLastField = NO;
break;
}
}
if (isLastField) {
[textView resignFirstResponder];
}
shouldChangeText = NO;
}
return shouldChangeText;
}
Found here: http://iphoneincubator.com/blog/tag/uitextfield
You'll want to implement UITextFieldDelegate's - (BOOL)textFieldShouldReturn:(UITextField *)textField method. An example of how to use this method to control the order is in this question.
I would like to elaborate on #sprocket's answer addressing the same issue. Just because something works out of the box doesn't mean you should stop thinking about a better way -- or even the right way -- of doing something. As he noticed the behavior is undocumented but fits our needs most of the time.
This wasn't enough for me though. Think of a RTL language and tabs would still tab left-to-right, not to mention the behavior is entirely different from simulator to device (device doesn't focus the first input upon tab). Most importantly though, Apple's undocumented implementation seems to only consider views currently installed in the view hierarchy.
Think of a form in form of (no pun intended) a table view. Each cell holds a single control, hence not all form elements may be visible at the same time. Apple would just cycle back up once you reached the bottommost (on screen!) control, instead of scrolling further down. This behavior is most definitely not what we desire.
So here's what I've come up with. Your form should be managed by a view controller, and view controllers are part of the responder chain. So you're perfectly free to implement the following methods:
#pragma mark - Key Commands
- (NSArray *)keyCommands
{
static NSArray *commands;
static dispatch_once_t once;
dispatch_once(&once, ^{
UIKeyCommand *const forward = [UIKeyCommand keyCommandWithInput:#"\t" modifierFlags:0 action:#selector(tabForward:)];
UIKeyCommand *const backward = [UIKeyCommand keyCommandWithInput:#"\t" modifierFlags:UIKeyModifierShift action:#selector(tabBackward:)];
commands = #[forward, backward];
});
return commands;
}
- (void)tabForward:(UIKeyCommand *)command
{
NSArray *const controls = self.controls;
UIResponder *firstResponder = nil;
for (UIResponder *const responder in controls) {
if (firstResponder != nil && responder.canBecomeFirstResponder) {
[responder becomeFirstResponder]; return;
}
else if (responder.isFirstResponder) {
firstResponder = responder;
}
}
[controls.firstObject becomeFirstResponder];
}
- (void)tabBackward:(UIKeyCommand *)command
{
NSArray *const controls = self.controls;
UIResponder *firstResponder = nil;
for (UIResponder *const responder in controls.reverseObjectEnumerator) {
if (firstResponder != nil && responder.canBecomeFirstResponder) {
[responder becomeFirstResponder]; return;
}
else if (responder.isFirstResponder) {
firstResponder = responder;
}
}
[controls.lastObject becomeFirstResponder];
}
Additional logic for scrolling offscreen responders visible beforehand may apply.
Another advantage of this approach is that you don't need to subclass all kinds of controls you may want to display (like UITextFields) but can instead manage the logic at controller level, where, let's be honest, is the right place to do so.
When a hardware keyboard is used with iOS, pressing tab or shift-tab automatically navigates to the next or previous logical responder, respectively. Is there a way to do the same programmatically (i.e. simulating the tab key rather than keeping track of the logical order manually)?
As William Niu is right but you can also use this code explained below.
I have used this and got success.Now consider the example of UITextField...
You can use UITextView's delegate method -(BOOL)textFieldShouldReturn:(UITextField*)textField as explained below.
But before doing this you should have to give tag to each UITextField in an Increment order...(Increment order is not required necessary ,but as for my code it is required, you can also use decrement order but some code changes for doing this)
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
NSInteger nextTag = textField.tag + 1;
UIResponder* nextResponder = [self.view viewWithTag:nextTag];
if (nextResponder) {
[nextResponder becomeFirstResponder];
} else {
[textField resignFirstResponder];
}
return YES;
}
Hope this will work for you...
Happy coding....
You may define the "tab-order" using the tag property. The following post describes how to find the next tag index to go to for UITextFields,
How to navigate through textfields (Next / Done Buttons).
Here is a modified version of the code from that post. Instead of removing keyboard at the last tag index, this following code would try to loop back to the first tag index.
-(BOOL)textFieldShouldReturn:(UITextField*)textField;
{
NSInteger nextTag = textField.tag + 1;
// Try to find next responder
UIResponder* nextResponder = [textField.superview viewWithTag:nextTag];
if (nextResponder) {
// Found next responder, so set it.
[nextResponder becomeFirstResponder];
return NO;
}
// Try to find the first responder instead...
// Assuming the first tag index is 1
UIResponder* firstResponder = [textField.superview viewWithTag:1];
if (firstResponder) {
// loop back to the first responder
[firstResponder becomeFirstResponder];
} else {
// Not found, so remove keyboard.
[textField resignFirstResponder];
}
return NO; // We do not want UITextField to insert line-breaks.
}
If you want an UI element other than UITextField, you should still be able to use the same logic, with a few more checks.
Not sure if this helps, but in the context of a UITextFields, if you implement UITextFieldDelegate, - (BOOL)textFieldShouldReturn:(UITextField *)textField will get called when the return key of the soft keyboard is pressed.
I've tried to hit directly on my laptop keyboard and it seemed to jump between all the textfields in the order in which you've added them to the view, but didn't go to any other types of fields (Buttons etc.).
key on the keyboard is simulating the key on the soft keyboard of the simulator, which works as expected.
I've got a UISearchBar in my interface and I want to customise the behaviour of the the small clear button that appears in the search bar after some text has been entered (it's a small grey circle with a cross in it, appears on the right side of the search field).
Basically, I want it to not only clear the text of the search bar (which is the default implementation) but to also clear some other stuff from my interface, but calling one of my own methods.
I can't find anything in the docs for the UISearchBar class or the UISearchBarDelegate protocol - it doesn't look like you can directly get access to this behaviour.
The one thing I did note was that the docs explained that the delegate method:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText;
is called after the clear button is tapped.
I initially wrote some code in that method that checked the search bar's text property, and if it was empty, then it had been cleared and to do all my other stuff.
Two problems which this though:
Firstly, for some reason I cannot fathom, even though I tell the search bar to resignFirstResponder at the end of my method, something, somewhere is setting it back to becomeFirstResponder. Really annoying...
Secondly, if the user doesn't use the clear button, and simply deletes the text in the bar using the delete button on the keyboard, this method is fired off and their search results go away. Not good.
Any advice or pointers in the right direction would be great!
Thanks!
Found the better solution for this problem :)
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{
if ([searchText length] == 0) {
[self performSelector:#selector(hideKeyboardWithSearchBar:) withObject:searchBar afterDelay:0];
}
}
- (void)hideKeyboardWithSearchBar:(UISearchBar *)searchBar{
[searchBar resignFirstResponder];
}
The answer which was accepted is incorrect. This can be done, I just figured it out and posted it in another question:
UISearchbar clearButton forces the keyboard to appear
Best
I've got this code in my app. Difference is that I don't support 'live search', but instead start searching when the user touches the search button on the keyboard:
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
if ([searchBar.text isEqualToString:#""]) {
//Clear stuff here
}
}
Swift version handling close keyboard on clear button click :
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.characters.count == 0 {
performSelector("hideKeyboardWithSearchBar:", withObject:searchBar, afterDelay:0)
}
}
func hideKeyboardWithSearchBar(bar:UISearchBar) {
bar.resignFirstResponder()
}
You could try this:
- (void)viewDidLoad
{
[super viewDidLoad];
for (UIView *view in searchBar.subviews){
for (UITextField *tf in view.subviews) {
if ([tf isKindOfClass: [UITextField class]]) {
tf.delegate = self;
break;
}
}
}
}
- (BOOL)textFieldShouldClear:(UITextField *)textField {
// your code
return YES;
}
I would suggest using the rightView and rightViewMode methods of UITextField to create your own clear button that uses the same image. I'm assuming of course that UISearchBar will let you access the UITextField within it. I think it will.
Be aware of this from the iPhone OS Reference Library:
If an overlay view overlaps the clear button, however, the clear button always takes precedence in receiving events. By default, the right overlay view does overlap the clear button.
So you'll probably also need to disable the original clear button.
Since this comes up first, and far as I can see the question wasn't really adequately addressed, I thought I'd post my solution.
1) You need to get a reference to the textField inside the searchBar
2) You need to catch that textField's clear when it fires.
This is pretty simple. Here's one way.
a) Make sure you make your class a , since you will be using the delegate method of the textField inside the searchBar.
b) Also, connect your searchBar to an Outlet in your class. I just called mine searchBar.
c) from viewDidLoad you want to get ahold of the textField inside the searchBar. I did it like this.
UITextField *textField = [self.searchBar valueForKey:#"_searchField"];
if (textField) {
textField.delegate = self;
textField.tag = 1000;
}
Notice, I assigned a tag to that textField so that I can grab it again, and I made it a textField delegate. You could have created a property and assigned this textField to that property to grab it later, but I used a tag.
From here you just need to call the delegate method:
-(BOOL)textFieldShouldClear:(UITextField *)textField {
if (textField.tag == 1000) {
// do something
return YES;
}
return NO;
}
That's it. Since you are referring to a private valueForKey I can't guarantee that it will not get you into trouble.
Best solution from my experience is just to put a UIButton (with clear background and no text) above the system clear button and than connect an IBAction
- (IBAction)searchCancelButtonPressed:(id)sender {
[self.searchBar resignFirstResponder];
self.searchBar.text = #"";
// some of my stuff
self.model.fastSearchText = nil;
[self.model fetchData];
[self reloadTableViewAnimated:NO];
}
Wasn't able to find a solution here that didn't use a private API or wasn't upgrade proof incase Apple changes the view structure of the UISearchBar. Here is what I wrote that works:
- (void)viewDidLoad {
[super viewDidLoad];
UITextField* textfield = [self findTextFieldInside:self.searchBar];
[textfield setDelegate:self];
}
- (UITextField*)findTextFieldInside:(id)mainView {
for (id view in [mainView subviews]) {
if ([view isKindOfClass:[UITextField class]]) {
return view;
}
id subview = [self findTextFieldInside:view];
if (subview != nil) {
return subview;
}
}
return nil;
}
Then implement the UITextFieldDelegate protocol into your class and overwrite the textFieldShouldClear: method.
- (BOOL)textFieldShouldClear:(UITextField*)textField {
// Put your code in here.
return YES;
}
Edit: Setting the delegate on the textfield of a search bar in iOS8 will produce a crash. However it looks like the searchBar:textDidChange: method will get called on iOS8 on clear.