49

I have this UITableView with custom cells that can get only predefined values, therefore I use a UIPickerView as their inputView. All is jolly good until I edit a field and need to show its updated value.

In order to make things clearer and easier to maintain, I made delegates and data sources as separate classes, and use notifications to make them interact with the tableView. So, after a value has been chosen from the UIPickerView, the tableView's data source gets notified, and in turn notifies the main ViewController that holds a reference to the tableView. From there I call

[_tableView reloadData];

and everything seems to work, except that the UIPickerView disappears, I think because the cells are regenerated and somewhere some resignFirstResponder is called, or something like that. Is there any other way to make the tableView updating its values without having to implement a custom method somewhere that does it, which would be quite ugly?

bneely
  • 9,083
  • 4
  • 38
  • 46
Morpheu5
  • 2,610
  • 6
  • 39
  • 72
  • 2
    Have you tried not to reload entire table view, only reload the affect row? try this method: -reloadRowsAtIndexPaths:withRowAnimation: – cxa Jun 20 '11 at 09:44
  • Resings? hehe Oh the difference swapping two letters makes. :-) – Richard Brightwell Jun 20 '11 at 09:45
  • @xan Thanks for the hint, I'll try that. @Richard LOL thanks, corrected :) – Morpheu5 Jun 20 '11 at 09:59
  • @xan Unfortunately that method results in the same behaviour (although it only reloads the chosen cell). Thanks anyway, I'll try to do this manually. – Morpheu5 Jun 20 '11 at 10:11
  • It does not look simple, you may need to provide more detailed information –  Jun 20 '11 at 09:51
  • Continuing with @cxa's line of thought: don't forget the `reloadSections:withRowAnimation:` method on `UITableView`. Using this worked for me. – Tim Arnold Jul 19 '13 at 18:42
  • Sequence `[tableView beginUpdates]`, `[tableView endUpdates]` worked for me (iOS7) – vokilam Jan 16 '14 at 06:51

14 Answers14

24

This reads like expected behavior - the picker belongs to a particular cell, that cell gets reloaded and is not the first responder any more. I guess one had to select a specific element anyway for the picker to appear, i.e. to make it first responder.

So you either need to make it become first responder again after reloading, or update the specific cell directly.

Eiko
  • 25,601
  • 15
  • 56
  • 71
22

I met the same problem, none of the answers above worked perfectly (I see the keyboard bouncing up and down, etc.).
Following this SO post I fixed the issue by calling

[tableView beginUpdates];
[tableView endUpdates]; 

this worked for me, table rows get updates and even expand/shrink (if you are changing rows height dynamically) with a nice animation, all without resigning first responder or even starting keyboard dismiss.
This will not scroll your table view to fit any expanded row, so I put the snippet above in dedicated method, f.e.:

- (void)tableView:(UITableView *)tableView reloadRowWhileShowingKeyboard:(NSIndexPath *)indexPath 
{  
    [tableView beginUpdates];
    [tableView endUpdates]; 

    [tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
Community
  • 1
  • 1
zutroi
  • 221
  • 2
  • 3
22

adding:

[yourSearchBar becomeFirstResponder];

after your:

[_tableView reloadData];

did the trick

valvoline
  • 7,737
  • 3
  • 47
  • 52
  • 9
    This worked for me in iOS 5. However in iOS 6 it doesn't work. After you call [_tableView reloadData] the data is not reloaded right away but after you return from your method. I had to rework my code to use UITableView insertXXX, reloadXXX and removeXXX methods instead of reloading all the data. If you know iOS 6 compatible method please share it. – demosten Sep 30 '12 at 22:15
  • Works for me on iOS 8 and 9 ;) – DarkLeafyGreen Sep 20 '15 at 18:09
  • 1
    //Put some delay for updating the animation and reload process of table //specially for ios11 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [yourSearchBar becomeFirstResponder]; }); This trick will work. – guru Jul 19 '18 at 10:35
  • if the user changes keyboard type from `.default` to `numbersAndPunctuation` and select any key then keyboard appears with .`default` type. Any solution for this ??????? – Hitesh Agarwal Jul 20 '18 at 13:12
9

You can follow this approach, not the best, but it works:

// pass the responder to a temporary (hidden) textField
[_tmpTextField becomeFirstResponder];

// reload data
[_tableView reloadData];

// reloadData is definitely async... 
// so pass the responder back in a timed op
double delayInSeconds = 0.1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [_textField becomeFirstResponder];
});
Carlos Ricardo
  • 2,058
  • 25
  • 32
  • 2
    I tried this. They keyboard seems to dismiss very briefly, then immediately reappear. Perhaps I did not make my `_tmpTextField` correctly. I'm just making one with `[[UITextInput alloc] init]`, then setting it as the first responder. Given that I'm in a UITableViewController instance, I can't think where else to put it. Help? – Diogenes Creosote Jun 05 '14 at 01:32
  • 2
    it's very interesting and it works for me!! Keyboard appear quicker if set `delayInSeconds` to 0.1 but if it set to 0.001 it works!. Maybe, it causes some performance issues... I don't know. – debiasej Sep 09 '15 at 19:58
8

I solved this by subclassing UITextView, overriding -(BOOL)resignFirstResponder and by adding a BOOL canResign. this variable is set before reloading the data and unset a short time after.

Joris Weimar
  • 4,783
  • 4
  • 33
  • 53
  • 1
    can you provide more details about your solution?? it will help me a lot.. thanks – DaSilva Jul 21 '14 at 14:51
  • i think i provided the details... you subclass UITextView override resignFirstResponder and only return ([super resignFirstResponder] && self.canResign); from your code you would set textView.canResign = NO... and then a performSelector:... afterDelay:... that sets canResign = YES – Joris Weimar Jul 28 '14 at 12:20
  • 2
    then you probably didn't implement it properly. it works fine for me. – Joris Weimar Oct 24 '14 at 09:43
3
customTextField.canResign = NO;
[self.tableView reloadData];
customTextField.canResign = YES;

Custom text field is derived from UITextField.

.h

@interface CustomTextField : UITextField
@property (nonatomic,assign) BOOL canResign;
@end

.m

- (BOOL)canResignFirstResponder
{
    return self.canResign;
}

Make sure that your custom text field is not recreated on table view reloading.

Krešimir Prcela
  • 4,257
  • 33
  • 46
3

I put my UISearchBar in its own section in a UITableView. When firing off the search, I made sure to only refresh the sections which do not contain the search bar.

- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
Jonny
  • 15,955
  • 18
  • 111
  • 232
3

Swift solution:

we can override default canResignFirstResponder by subclassing UITextfiled

class CustomField: UITextField{
    var canResign:Bool = false
    override var canResignFirstResponder: Bool{
        return canResign
    }  
}

all you need to set canResign variable before and after reload statement.

cell.offerInputTextField.canResign = false
tableView.reloadData()
cell.offerInputTextField.canResign = true

don't forget to assign the custom class text field as CustomField.

  • 3
    As of iOS 13, this does not work for me anymore. Returning `false` will cause the reload to fail – below Oct 06 '20 at 23:05
1

Here my solution. Think that search textfield is in index 0.. You need to update or add or remove next rows..

let beforeCount = self.filteredItems.count
self.filteredItem = ... next filtered items

var removalIndexPaths = [IndexPath]()
var appendalIndexPaths = [IndexPath](
var modifyIndexPaths = [IndexPath]()
self.tableView.beginUpdates()
let deleteAvailable = beforeCount != 0
if deleteAvailable {
    for i in stride(from: beforeCount - 1, to: 0, by: -1) {
        removalIndexPaths.append(IndexPath(row: i, section: 0))
     }
}
        
let appendAvailable = self.filteredShots.count != 0
if appendAvailable {
     for i in stride(from: 1, to: self.filteredShots.count, by: 1) {
         appendalIndexPaths.append(IndexPath(row: i, section: 0))
      }
 }
        
 modifyIndexPaths = appendalIndexPaths.filter { appendPath in
      if appendPath == removalIndexPaths.filter({ $0 == appendPath }).first {
         return true
     } else {
         return false
     }
 }
        
for item in modifyIndexPaths {
     if let rai = appendalIndexPaths.firstIndex(where: { $0 == item}) {
          appendalIndexPaths.remove(at: rai)
     }
     if let rri = removalIndexPaths.firstIndex(where: { $0 == item}) {
           removalIndexPaths.remove(at: rri)
      }
}
        
if modifyIndexPaths.count > 0 {
      self.tableView.reloadRows(at: modifyIndexPaths, with: .none)
}
        
if removalIndexPaths.count > 0 {
      self.tableView.deleteRows(at: removalIndexPaths, with: .none)
}
        
if appendalIndexPaths.count > 0 {
       self.tableView.insertRows(at: appendalIndexPaths, with: .none)
}
               
self.tableView.endUpdates()
Gokhan Alp
  • 61
  • 5
0

If you are facing this issue with a search bar, the following did it for me in iOS 6:

  • Instantiate a UISearchBar and add it as a subview to your UITableView at the top.
  • Create a dummy first cell in your UITableView so that the search bar only blocks this dummy cell and not your actual cell with data.
adbie
  • 540
  • 4
  • 13
0

As mentioned by @Eiko, this works for me!

Update the cell in UIPickerViewDelegate's pickerView:didSelectRow:inComponent: method:

- (void) pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
  TableViewCell *cell = (TableViewCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:pickerView.tag inSection:0]];

  /*
  Update your cell here. 
  */

  // Reload TableViewCell will resign the PickerView, so we need to focus it back.  
  [self.tableView reloadData];
  NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
  NSArray* indexArray = [NSArray arrayWithObjects:indexPath, nil];
  [self.tableView reloadRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationNone];
  [cell.textField becomeFirstResponder];
}
Hanton
  • 616
  • 1
  • 6
  • 13
0

Track which cell's keyboard is active and then get that particular cell by cellForRowAtIndexPath and make textView firstResponder

self.tableView.reloadData()
if let indexPath = self.activeIndexPath{
   if let cell = createFormTableView.cellForRow(at: indexPath) as? TextViewTableViewCell {
        cell.txtViewInput.becomeFirstResponder()
   }
}
Hitesh Agarwal
  • 1,943
  • 17
  • 21
0

I use beginUpdate and endUpdate After end update, get the cell contains the textfield already has focus then make it first responder

    self.tableView.beginUpdates()
    self.tableView.reloadRows(at: [indexPath], with: .automatic)
    self.tableView.endUpdates()
    let newCell = self.tableView.cellForRow(at: indexPath)
    newCell.textField.becomeFirstResponder()
Omar HossamEldin
  • 3,033
  • 1
  • 25
  • 51
-1

You can solve this issue by temporarily transferring the first responder status to other object. Usually you transfer the control of input view to your ViewController. Since your UIViewController also inherits from UIResponder, you can do something like this:

on didSelect { ....

[yourViewController becomeFirstRespoder];

[_tableView reloadData];

[yourInputField becomeFirstResponder];

.... }

Thus, once the table is reloaded, you can transfer firstResponder status back to your label/field. By default, the canBecomeFirstResponder is set to NO. So you might need to override the same in your ViewController. Also, you might need to make the inputView for your view controller the same as your UIPicker, otherwise it might just dismiss your picker and display a keyboard.

Kevin Wadera
  • 529
  • 4
  • 3