UITableView crash on updates after text edit - iphone

So I'm totally stumped by this one and tempted to call "OS bug".
I have a TableView controller with a single section, and in the header for that section there is an UITextField. Several operations result in rows being added/removed without a problem. However, as soon as text is edited in the header, and the keyboard dismissed, any insertion/removal of rows results in an immediate crash.
And it can actually be simplified further - simply calling beginUpdates/endUpdates on the table once the keyboard is dismissed is enough to cause a crash. The end of the callstack is:
_CFTypeCollectionRetain
_CFBasicHashAddValue
CFDictionarySetValue
-[UITableView(_UITableViewPrivate) _updateWithItems:withOldRowData:oldRowRange:newRowRange:context:]
-[UITableView(_UITableViewPrivate) _endCellAnimationsWithContext:]
-[UITableView endUpdates]
I've put together a minimal example that demonstrates the problem.
Complete Controller source: http://www.andrewgrant.org/public/TableViewFail.txt
Example Project: http://www.andrewgrant.org/public/TableViewCrash.zip
Most relevant code:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
// create header view
UIView* header = [[[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, 320.f, 50.f)] autorelease];
// text field
UITextField* textField = [[[UITextField alloc] initWithFrame:CGRectMake(10.f, 12.f, 300.f, 28.f)] autorelease];
textField.text = #"Edit, then 'Save' will crash";
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.clearButtonMode = UITextFieldViewModeAlways;
textField.delegate = self;
[header addSubview:textField];
return header;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
// no purpose, but demonstrates updates work at this point
[self.tableView beginUpdates];
[self.tableView endUpdates];
[textField resignFirstResponder];
// immediate crash
[self.tableView beginUpdates];
[self.tableView endUpdates];
return YES;
}

Just an update - I submitted a bug report and repro case to Apple and they confirmed it's a bug in iOS 4.0. As of iOS 4.1 beta 2 it had not been fixed.
My work around was to turn the first row of my table into a pseudo header that occupies the entire content view and has a custom height. It's not quite as good (things cant reach to the edge of the screen for example) but it's close and doesn't crash.

I hit this bug last night and spent a few hours this morning trying to figure it out. The other answers in this thread didn't work for me, but did help me come up with a workaround that I think is best.
Cameron suggested making an offscreen UITextField the firstResponder, then resigning it before calling endUpdates on the tableview. This didn't work for me, but it gave me an idea.
In the context of my custom header view, I re-parent the text field (in my case, a UISearchBar, actually) before calling resignFirstResponder. Then I put it back:
[self.window addSubview: sb];
[sb resignFirstResponder];
[self addSubview: sb];
a few lines later, when I call [tableView endUpdates], it no longer crashes.
Edit: It just got a bit more complicated. The problem is that if first-responder status is otherwise revoked (for example, the user dismisses the keyboard), this parent-swapping code isn't executed, and we'll eventually get the crash. My current fix is to place a category-override on UITextField resignFirstResponder -- seems to work but not sure yet if there are any adverse side effects.
#implementation UITextField (private)
- (BOOL) resignFirstResponder
{
UIView* superviewSave = self.superview;
[self.window addSubview: self];
BOOL success = [super resignFirstResponder];
[superviewSave addSubview: self];
return success;
}
#end

Taking Tom's solution one step further, I noticed this solution only works on iOS 4.X, which is ok because this problem only exists in iOS 4.X. Therefore I changed his method to:
#implementation customUITextField
- (BOOL)resignFirstResponder {
if ( [[UIDevice currentDevice].systemVersion characterAtIndex:0] == '4' ) {
UIView* superviewSave = self.superview;
[self.window addSubview:self];
BOOL success = [super resignFirstResponder];
[superviewSave addSubview:self];
return success;
}
return [super resignFirstResponder];
}
#end

I came across this bug myself and am so glad I found your post because I was banging my head on my desk trying to figure out where I screwed up.
For my workaround, I created an offscreen UITextField and called becomeFirstResponder and then resignFirstResponder on that text field before doing the updates. This avoided the crash and didn't require any redesign of the headers or cells.

Related

motion callbacks never called

I'm trying to make a shake events.
I tried:
1) How do I detect when someone shakes an iPhone? (posts of Kendall, and Eran)
2) motionBegan: Not Working
but nothig helps.
My View becomes first responder, but motionBegan/motionEnded never called.
Is there some additiol settings must be done, or i'm missing somethig? My iOS SDK is 4.3.
I have a class of UIView:
#import "ShakeView.h"
#implementation ShakeView
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
NSLog (#"123");
if ( event.subtype == UIEventSubtypeMotionShake ) {
NSLog(#"Shake!");
}
if ([super respondsToSelector:#selector(motionEnded:withEvent:)]) {
[super motionEnded:motion withEvent:event];
}
}
#end
In my ViewController's xib class of View is ShakeView.
my ViewController pushed:
Wheel *secondViewController = [[Wheel alloc] initWithNibName:#"Wheel" bundle:nil];
[self.navigationController pushViewController:secondViewController animated:YES];
[secondViewController release];
In my ViewController:
- (void) viewDidAppear:(BOOL)animated
{
[self.view becomeFirstResponder];
[super viewWillAppear:animated];
NSLog(#"%d", [self.view isFirstResponder]);
}
- (void) viewWillDisappear:(BOOL)animated
{
[self.view resignFirstResponder];
[super viewWillDisappear:animated];
}
It logs "1", so it IS first responder. But it logs nothing else.
I spend a half day on this few lines of code, and I have no more ideas. Do anyone knows how to solve it?
Thanks.
This is much too late to help SentineL, but I was having the same problem and I like his question because it is clear that he has all the relevant code in place -- except one crucial line, in the application delegate's didFinishLaunching:
[self.window makeKeyAndVisible];
This is very hard to debug, because even without this line, everything else will be fine. Your gestures will work, your controls will respond, you will be able to make your view first responder (as SentineL checked) -- but your subclassed window or view or view controller will never receive the motion events.
Which doesn't make sense to me. Why would makeKeyAndVisible affect the accelerometer but not gestures? Hopefully some more experienced user can answer that.
P.S. If you use this code as an example, I would recommend that you omit the super respondsToSelector conditional. Of course it responds to the selector; you're overriding it.

EXC_BAD_ACCESS when using resignFirstResponder on textFieldShouldReturn for iphone

I have a textfield that i want to hide when the user presses the return button. the textfield was created in the interface builder, i added the textfield delegate in my .h file, and set the delegate for the textfield as the file owner.
#interface ProfileEdit : UIViewController<UITextFieldDelegate>{
UITextField *textfield1;
UITextField *textfield2;
UITextField *textfield3;
}
- (void)viewDidLoad
{
textfield1 = [[UITextField alloc] initWithFrame:CGRectMake(20, 49, 164, 31)];
[textfield1 setDelegate:self];
[textfield1 setAutocorrectionType:UITextAutocorrectionTypeNo];
[self.view addSubview:textfield1];
textfield2 = [[UITextField alloc] initWithFrame:CGRectMake(20, 124, 164, 31)];
[textfield2 setDelegate:self];
[textfield2 setAutocorrectionType:UITextAutocorrectionTypeNo];
[self.view addSubview:textfield2]
textfield3 = [[UITextField alloc] initWithFrame:CGRectMake(20, 198, 164, 31)];
[textfield3 setDelegate:self];
[textfield3 setAutocorrectionType:UITextAutocorrectionTypeNo];
[self.view addSubview:textfield3];
[super viewDidLoad];
}
I also put a button in the background, where the touchupinside event triggers
-(IBAction)hideKeyboard:(id)sender{
[textfield1 resignFirstResponder];
[textfield2 resignFirstResponder];
[textfield3 resignFirstResponder];
}
This works fine, no errors. But for this
-(BOOL)textFieldShouldReturn:(UITextField *)textField{
[self hideKeyboard:nil];
return YES;
}
I get the EXC_BAD_ACCESS in the main.m. I've been stuck on this for a couple days and have no idea why this is happening.
I tested this on my iphone and there was no error. I think it has to do with the simulator itself. I found that if i put a delay on the [testField resignFirstResponder], no error is thrown in the simulator
For any EXC_BAD_ACCESS errors, you are usually trying to send a message to a released object. The BEST way to track these down is use NSZombieEnabled.
This works by never actually releasing an object, but by wrapping it up as a "zombie" and setting a flag inside it that says it normally would have been released. This way, if you try to access it again, it still know what it was before you made the error, and with this little bit of information, you can usually backtrack to see what the issue was.
It especially helps in background threads when the Debugger sometimes craps out on any useful information.
VERY IMPORTANT TO NOTE however, is that you need to 100% make sure this is only in your debug code and not your distribution code. Because nothing is ever released, your app will leak and leak and leak. To remind me to do this, I put this log in my appdelegate:
if (getenv("NSZombieEnabled"))
NSLog(#"NSZombieEnabled enabled!");
If you need help finding the exact line, Do a Build-and-Debug (CMD-Y) instead of a Build-and-Run (CMD-R). When the app crashes, the debugger will show you exactly which line and in combination with NSZombieEnabled, you should be able to find out exactly why.
Change return value of textFieldShouldReturn to NO.
I had this problem even without resignFirstResponder call when I was displaying a view controller.
You should re-write the hideKeyboard method to:
- (IBAction)hideKeyboard:(id)sender; {
if ([textfield1 isFirstResponder]) {
[textfield1 resignFirstResponder];
}
if ([textfield2 isFirstResponder]) {
[textfield2 resignFirstResponder];
}
if ([textfield3 isFirstResponder]) {
[textfield3 resignFirstResponder];
}
}
Also, make sure that the outlets are all hooked up properly in the nib, and that your file is a UITextFieldDelegate, and the textfields should have their delegate hooked up to file owner in the nib. That should (hopefully) fix the problem.
EDIT: You should try putting the 3 UITextFields in the nib, and hooking them up that way. It usually results in less problems, and it is easier to change the design later if you need to.
Hope that helps!

What is the right place to add a subview dependent on scrollView.contentSize?

I'm using Cocoanetic's pull-to-reload but with a twist: I would like the UITableView to be able to pull up, as it were, to load more data.
I customized the classes and have managed to adjust all functionality to support this. What got me stumped, basically, is where in my code to create and add the extra view.
I first tried to add it in viewDidLoad of Cocoanetic's class (a UITableViewController):
- (void)viewDidLoad
{
[super viewDidLoad];
refreshHeaderView = [[EGORefreshTableHeaderView alloc] initWithFrame:CGRectMake(0.0f, 0.0f - self.view.bounds.size.height, 320.0f, self.view.bounds.size.height)];
refreshFooterView = [[EGORefreshTableFooterView alloc] initWithFrame:CGRectMake(0.0f, 0.0f + self.tableView.contentSize.height, 320.0f, 20.0f)];
[self.tableView addSubview:refreshHeaderView];
[self.tableView addSubview:refreshFooterView];
self.tableView.showsVerticalScrollIndicator = YES;
}
This does not work, as self.tableView.contentSize.height is zero at this point, because the table hasn't loaded it's data yet.
Not to worry, I thought, and tried to add it in the viewDidLoad of the UITableViewController subclass I made:
- (void)viewDidLoad
{
[super viewDidLoad];
// stuff
self.model = [ListingModel listingModelWithURL:#"avalidurl" delegate:self];
refreshFooterView = [[EGORefreshTableFooterView alloc] initWithFrame:CGRectMake(0.0f, 0.0f + self.tableView.contentSize.height, 320.0f, 20.0f)];
[self.tableView addSubview:refreshFooterView];
}
Note I set the model first, but that also didn't work, for the same reason. I assume the table hasn't been layed-out yet. In frustration I gave my class a BOOL property and an addFooter method (the BOOL to make sure it's only called once) called from tableView:cellForRowAtIndexPath: which obviously is a far cry from The Right Way™
So what would, given this scenario, be The Right Way™?
The solution was easier than I thought and 7KV7 actually gave me the hint I needed.
- (void)viewDidLoad
{
[super viewDidLoad];
// stuff
self.model = [ListingModel listingModelWithURL:#"avalidurl" delegate:self];
/*
We're forcing a reload of the table here, that way the table has a
contentSize, so adding the footerView now works correctly.
*/
[self.tableView reloadData];
[self addRefreshFooter];
}
From this previous SO question Get notified when UITableView has finished asking for data? subclassing UITableView's reloadData is the best approach :
- (void)reloadData {
NSLog(#"BEGIN reloadData");
[super reloadData];
NSLog(#"END reloadData");
}
reloadData doesn't end before the table has finish reload its data. So, when the second NSLog is fired, the table view has actually finish asking for data.
If you've subclassed UITableView to send methods to the delegate before and after reloadData. It works like a charm.

How to set text in UISearchBar without activating UISearchDisplayController

I'm using a UISearchDisplayController in my app. When the user selects an item in the search results returned I deactivate the UISearchDisplayController. Deactivating the controller clears the text the user has typed. I want to keep it there. I can force the text back into the UISearchBar by setting it again after the controller has been deactivated.
Like so:
NSString* searchText = self.searchDisplayController.searchBar.text;
[self.searchDisplayController setActive:NO animated:YES];
self.searchDisplayController.searchBar.text = searchText;
Which works.
However, I am seeing a timing issue if I don't animate the deactivate call. Calling
setActive like so:
NSString* searchText = self.searchDisplayController.searchBar.text;
[self.searchDisplayController setActive:NO animated:NO];
self.searchDisplayController.searchBar.text = searchText;
causes the UISearchDisplayController to become active again!
Is there are a way that I can set the text of the UISearchBar without having the UISearchDisplayController that's associated with become active? Any other suggestions to get around this behaviour?
For any one else wondering how to do this I managed to get it working by adding this in my delegate:
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
if(!controller.isActive){
controller.searchResultsTableView.hidden = YES;
return NO;
}
controller.searchResultsTableView.hidden = NO;
[....]
return YES;
}
Aaron answer works fine. A simpler way of doing things, by editing your peace of code:
NSString* searchText = self.searchDisplayController.searchBar.text;
[self.searchDisplayController setActive:NO animated:NO];
self.searchDisplayController.delegate = nil;
self.searchDisplayController.searchBar.text = searchText;
self.searchDisplayController.delegate = self; //or any delegate you like!
That way, none of your delegate functions are going to be triggered when setting the search bar text.
In Apple's internal forum someone suggested a workaround of setting the placeholder text of the searchBar to the last search text when the UISearchDisplayController deactivates. It appears in the box, but it's greyed out. Not ideal, but possibly acceptable.

Keyboard not showing after MFMessageComposeViewController

In an iPhone app I have a UITextView and a button, which lets the user send the content of the UITextView as a text message. The code looks like this:
MFMessageComposeViewController *picker = [[MFMessageComposeViewController alloc] init];
picker.messageComposeDelegate = self;
picker.body = textView.text;
[self presentModalViewController:picker animated:YES];
Everything works fine, except for when the message is either sent or Cancel is tapped in the MFMessageComposer: The keyboard for the UITextView is not shown anymore, even though the cursor blinks.
I tried a few things, including a [textView resignFirstRepsonder] in both the button code and -viewDidDisappear. [textView becomeFirstResponder] in the MFMessageComposeViewControllerDelegate method or the -viewDidAppear didn't change anything either...
Any ideas?
I had the same issue, and was resigned to accepting fabian's solution, but found that by calling [self dismissModalViewControllerAnimated:NO] and then calling [textView becomeFirstResponder], I was able to make the keyboard reappear. Something about the animation was screwing up the keyboard; looks like a bug in iOS 4.2.
After the view has disappeared, you need to make your view first responder. Add the MFMessageComposeViewControllerDelegate protocol to your header, then use the following:
- (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result{
[self dismissModalViewControllerAnimated:YES];
[self becomeFirstResponder];
}
Happy coding,
Zane
I had a similar problem and was able to fix it by calling becomeFirstResponder after a slight delay:
[textField performSelector:#selector(becomeFirstResponder) withObject:nil afterDelay:0.01];
The delay trick also solves the problem of missing text cursor after showing an UIAlert right after MFMessageComposeViewController finishes, however the delay needs to be much longer (0.5 sec in my case)
I was not able to find a better solution, so here is my fix:
In
- (void) actionSheet:(UIActionSheet *)actionSheet
willDismissWithButtonIndex:(NSInteger)buttonIndex
I dismiss the Keyboard and in
- (void) actionSheet:(UIActionSheet *)actionSheet
didDismissWithButtonIndex:(NSInteger)buttonIndex`
I present the MFMessageComposeViewController.
In
- (void)messageComposeViewController:(MFMessageComposeViewController *)controller
didFinishWithResult:(MessageComposeResult)result
I don't do [textView becomeFirstResponder] as it doesn't work. Neither does it work in viewDidAppear:. The user has to tap the UITextField again.
Not a very nice solution but the only one I found...
As of iOS 5, here is one workaround. Before you present the MFMessageComposeViewController instance, resign first responder on your UITextView:
[self presentViewController:messageComposer animated:YES completion:NULL];
[textView resignFirstResponder];
Then in the delegate method messageComposeViewController:didFinishWithResult: do this:
[controller dismissViewControllerAnimated:YES completion:^{
[textView performSelector:#selector(becomeFirstResponder) withObject:nil afterDelay:0];
}];
This fixed the disappearing keyboard problem for me. Without having to permanently dismiss the keyboard.
This behaviour will not appear if viewController which is shown before modal VC is a child of navigation controller. So solution is to make fake UINavigationController and add your VC controller to nav controller.