iOS Why would I get nil for [self.tableView cellForRowAtIndexPath:indexPath]?

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
Ok, I'm really confused by this behavior and quite frankly I just don't get it. The short part is that I've got a tableView with a bunch of custom cells that contain UITextFields. I've added a toolbar (as the inputAccessoryView) that shows up above the keyboard to allow for quick movement up and down (previous and next, respectively) via a UISegmentedControl in that toolbar. This all works fine UNTIL in the action handler for the UISegmentedControl somehow gets a nil back from cellForRowAtIndexPath: which is a valid index path for the tableView. Why would this return nil???

Here's the code of the handler method and friends:
Code:
-(void)prevNextAction:(UISegmentedControl *)control
{
    NSLog(@"[%@ %@] self.currentTextField=%@", NSStringFromClass([self class]), NSStringFromSelector(_cmd),self.currentTextField);
    
    if (control.selectedSegmentIndex == PREVIOUS_INDEX) {
        [self moveToPreviousTextField:self.currentTextField];
    } else {
        [self moveToNextTextField:self.currentTextField];
    }
}

-(void)moveToNextTextField:(UITextField *)textField
{
    NSLog(@"[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));

    if (!self.currentTextField) {
        NSLog(@"currentTextField is nil?");
    }
    
    if (textField != self.currentTextField) {
        NSLog(@"textField != currentTextField???");
        abort();
    }
    NSIndexPath *myIndexPath = [self indexPathForTextField:textField];
    NSIndexPath *nextTextFieldIndexPath = [self nextTextFieldIndexPathForIndexPath:myIndexPath];
    
    if (nextTextFieldIndexPath) {
        MCTextFieldCell *nextCell = (MCTextFieldCell *)[self.tableView cellForRowAtIndexPath:nextTextFieldIndexPath];
        self.currentTextField = nextCell.textField;
        [self.currentTextField becomeFirstResponder];        
    } else {
        [textField resignFirstResponder];
        self.currentTextField = nil;
    }
}

-(void)moveToPreviousTextField:(UITextField *)textField
{
    NSIndexPath *myIndexPath = [self indexPathForTextField:textField];
    NSIndexPath *prevTextFieldIndexPath = [self previousTextFieldIndexPathForIndexPath:myIndexPath];
    
    if (prevTextFieldIndexPath) {
        MCTextFieldCell *prevCell = (MCTextFieldCell *)[self.tableView cellForRowAtIndexPath:prevTextFieldIndexPath];
        if (prevCell) {
            self.currentTextField = prevCell.textField;
            [self.currentTextField becomeFirstResponder];
        } else {
            [textField resignFirstResponder];
            self.currentTextField = nil;
        }
    } else {
        [textField resignFirstResponder];
        self.currentTextField = nil;
    }    
}
(Ok, I don't want to put everything here, so here's a summary of some of the methods not shown - these seem to work OK:
  • indexPathForTextField: returns the indexPath for the cell that contains the current textField. It does this by finding the cell that holds the textField (which is textField.superview.superview) and asking the tableView for its indexPath via indexPathForCell:
  • nextTextFieldIndexPathForIndexPath:indexPath indexes a dictionary that maps a given indexPath to the next index path
  • previousTextFieldIndexPathForIndexPath:indexPath indexes a dictionary that maps a given indexPath to the previous indexPath)

I set a breakpoint at the line in moveToPreviousTextField: where it resignsFirstResponder if the previous cell is nil, mostly because the program should never get here - there should be a cell. Given that, I did a little poking and here's the results:
Code:
(lldb) po myIndexPath
(NSIndexPath *) $17 = 0x06b888e0 <NSIndexPath 0x6b888e0> 2 indexes [2, 0]
(lldb) po prevTextFieldIndexPath
(NSIndexPath *) $18 = 0x0a54dc40 <NSIndexPath 0xa54dc40> 2 indexes [1, 0]
(lldb) po _currentTextField
(UITextField *) $19 = 0x06b76030 <UITextField: 0x6b76030; frame = (83 12; 202 21); text = '0.00'; clipsToBounds = YES; opaque = NO; autoresize = W+H; layer = <CALayer: 0x6b764e0>>
(lldb) po [[self tableView] cellForRowAtIndexPath:myIndexPath]
(id) $20 = 0x06b763d0 <MCTextFieldCell: 0x6b763d0; baseClass = UITableViewCell; frame = (0 305; 320 47); autoresize = W; layer = <CALayer: 0x6b78fe0>>
(lldb) po [[self tableView] cellForRowAtIndexPath:prevTextFieldIndexPath]
(id) $21 = 0x00000000 <nil>
(lldb) po  [[self tableView] cellForRowAtIndexPath:(NSIndexPath *)[NSIndexPath indexPathForRow:1 inSection:2]]
(id) $22 = 0x06b7ac80 <MCTextFieldCell: 0x6b7ac80; baseClass = UITableViewCell; frame = (0 352; 320 46); autoresize = W; layer = <CALayer: 0x6b7a5d0>>
(lldb) po  [[self tableView] cellForRowAtIndexPath:(NSIndexPath *)[NSIndexPath indexPathForRow:0 inSection:2]]
(id) $23 = 0x06b763d0 <MCTextFieldCell: 0x6b763d0; baseClass = UITableViewCell; frame = (0 305; 320 47); autoresize = W; layer = <CALayer: 0x6b78fe0>>
(lldb) po  [[self tableView] cellForRowAtIndexPath:(NSIndexPath *)[NSIndexPath indexPathForRow:0 inSection:1]]
(id) $24 = 0x00000000 <nil>
(lldb) po  [[self tableView] cellForRowAtIndexPath:(NSIndexPath *)[NSIndexPath indexPathForRow:0 inSection:0]]
(id) $25 = 0x00000000 <nil>
So why does cellForRowAtIndexPath return nil for [1,0] and [0,0] - I just don't get it at all and need some new place to look for an answer. Please help!
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
Code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"[%@ %@] indexPath = %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), indexPath);

    UITableViewCell *cell = nil;
    
    cell = [tableView dequeueReusableCellWithIdentifier:@"MCTextFieldCell"];
    
    if (!cell) {
        NSLog(@"cell == nil???");
        abort();
    }
    
    [cell prepareForReuse]; // is this necessary???
    return [self configureCell:(MCTextFieldCell *)cell
                   atIndexPath:indexPath];
}

-(MCTextFieldCell *)configureCell:(MCTextFieldCell *)cell
                      atIndexPath:(NSIndexPath *)indexPath
{    
    Variable *var = [self.variableFRC objectAtIndexPath:indexPath];
    
    cell.indexPath = indexPath;
    cell.textField.delegate = self;
    cell.textField.text = [self.model variableValueAsString:var];
    cell.textField.keyboardType = UIKeyboardTypeDecimalPad;
    MCPrevNextDoneConfig config = kPrevNextDoneMiddleField;
    
    if ([indexPath isEqual:self.firstIndexPath]) {
        if ([self.lastIndexPath isEqual:self.firstIndexPath]) {
            config = kPrevNextDoneOnlyField;
        } else {
            config = kPrevNextDoneFirstField;
        }
    } else if ([indexPath isEqual:self.lastIndexPath]) {
        config = kPrevNextDoneLastField;
    }
    
    cell.textField.inputAccessoryView = [self prevNextDoneToolbarWithConfig:config];
    cell.label.text = var.label;
    [self updateCell:cell labelColor:[self.model variableNameColor:var]];
    cell.label.textAlignment = UITextAlignmentRight;
    cell.label.lineBreakMode = UILineBreakModeWordWrap;
    return cell;
}
Variable is a CoreData class that holds some information about the value corresponding to the cell, and it has a printable name (label) and an associated value (accessible as a string using the model object). Variables might also be computed as the result of some other variable being entered (for example, in the simple case of A=B+C, when values are entered for A and B, the value for C is computed), and in that case the label of the cell is assigned an appropriate color via updateCell:labelColor. The inputAccessoryView of the textField is pulled from a cache of one of 4 kinds of toolbars - one with no next or previous, one with a next only, one with both previous and next, and one with a previous only - with the prevNextDoneToolbarWithConfig: method.

I presume from your question that when I ask the tableView for a cell that isn't visible or cached, the tableview is going to ask its dataSource for a cell for that indexPath. Does that seem to match your expectations? If so, I don't understand why this code might return nil without at least whining about it...
 

idelovski

macrumors regular
Sep 11, 2008
235
0
Could be something else, but -cellForRowAtIndexPath: returns a cell only for visible cells. All the other cells don't exist until system calls your -tableView:cellForRowAtIndexPath: to provide one.
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
Could be something else, but -cellForRowAtIndexPath: returns a cell only for visible cells. All the other cells don't exist until system calls your -tableView:cellForRowAtIndexPath: to provide one.
Right. I'm asking the system to return one for a cell that's not visible (via the self.tableView cellForRowAtIndexPath: method call), and that's the one that's returning nil.
 

dejo

Moderator
Staff member
Sep 2, 2004
15,981
450
The Centennial State
Ah, that's the difference. [self.tableView cellForRowAtIndexPath:indexPath] is not the same as UITableViewDataSource's tableView:cellForRowAtIndexPath: method.
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
Ah, that's the difference. [self.tableView cellForRowAtIndexPath:indexPath] is not the same as UITableViewDataSource's tableView:cellForRowAtIndexPath: method.
Truth.

Ok, I'm an idiot - the documentation clearly says that cellForRowAtIndexPath: only returns cells that are visible. Maybe we should learn to read the documentation, huh...

Great. Now I have to check if something is on the screen and make a decision based on that, somewhat micromanaging the tableView behavior. I don't really like it, so I'm going to try a workaround (possible hack?): I'm going to ask the tableView to scroll the prevTextFieldIndexPath so it's visible THEN I'm going to ask for the cell for that indexPath and see if that helps; I'll report back on how this works.
 

TheWatchfulOne

macrumors 6502
Jun 19, 2009
411
292
I'm certainly no expert but here's a question I want to pose:

First a little build up:

When I read about table view cells are queued are dequeued I get the impression that however many cells will fit on the screen is all the instances of a cell you get. This is done to conserve memory which is limited on portable devices. Correct so far?

So let's say my table view can display 10 cells on the scrreen at a time. (10 being an arbitrary number.) The normal way to make cells visible would be to scroll the table view. This means I can make new cells visible because I'm releasing other cells.

Now let's say I implement functionality such as that being discussed here. If I understand correctly this navigates the table view by using arrow buttons to "go the next record." Is that correct? If so, then does going to the "next (or previous) record" scroll the table view? If it does not scroll the table view, then what would happen if my current record was the cell at the top of the screen and I tried to "go to the previous record"? If the table view is not being scrolled, then cells are not being released. If you don't release a cell then you can't make a new cell visible? Do I understand correctly what is happening?

Looking forward to further discussing.:cool:

Edit: Looks like Ron beat me to it. It takes me too long to type these days.

----------

Truth.

Ok, I'm an idiot - the documentation clearly says that cellForRowAtIndexPath: only returns cells that are visible. Maybe we should learn to read the documentation, huh...

Great. Now I have to check if something is on the screen and make a decision based on that, somewhat micromanaging the tableView behavior. I don't really like it, so I'm going to try a workaround (possible hack?): I'm going to ask the tableView to scroll the prevTextFieldIndexPath so it's visible THEN I'm going to ask for the cell for that indexPath and see if that helps; I'll report back on how this works.
The keyboard covers the lower half of the screen anyway. I know you can tell the table view to scroll to exactly the point you want it to. You could just set it to scroll so that the "current cell" is always the second one from the top. Maybe make it so the user can't scroll the table view while the keyboard is visible. As a user, I would rather scroll with the arrow buttons than the tiny portion of the table view that is visible "behind the keyboard". And I don't think that would be hackish. My opinion of course. Looking forward to hearing how it works out for you.:cool:
 

PhoneyDeveloper

macrumors 68040
Sep 2, 2008
3,114
93
@Ron,

You're not getting how this model view controller thing is supposed to work. Just make the change to your data model. You can update the cell directly if it exists but if it isn't visible then if it is later scrolled on screen it should appear correct based on your data model and the code in cellForRowAtIndexPath. If this doesn't work there's a bug in your code.

Regarding textfields in table views: you should always save the text from the text field on every keystroke in your data model. If you don't you get problems when the text fields are scrolled offscreen.
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
@Ron,

You're not getting how this model view controller thing is supposed to work. Just make the change to your data model. You can update the cell directly if it exists but if it isn't visible then if it is later scrolled on screen it should appear correct based on your data model and the code in cellForRowAtIndexPath. If this doesn't work there's a bug in your code.

Regarding textfields in table views: you should always save the text from the text field on every keystroke in your data model. If you don't you get problems when the text fields are scrolled offscreen.
@PhoneyDeveloper - Truly that's not the issue here; you're reading my issue as if I'm using the textField as the backing store, but I'm not - it's being store in the model. The problem at hand is moving the focus to the next or previous text field (in the next or previous cell) when the next or previous button is pressed. Nothing is stored in the textField other than a copy of the value (see, for example, the configureCell:atIndexPath: method shown above).

Here's a little more code to help convince you that I'm using the model the right way. The interface is kinda mid-factoring (meaning I'm not really thrilled with it, but it seems to work, just feels a little wonky to me).
Code:
-(BOOL)textFieldShouldClear:(UITextField *)textField
{
    [self updateValue:0.00 
         forTextField:textField
           isClearing:YES];
    
    return YES;
}

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSString *theString = textField.text;
    
	if (range.length > 0) {
		theString = [theString stringByReplacingCharactersInRange:range withString:@""];
	} else if (range.location == [theString length]) {
        theString = [theString stringByAppendingString:string];
    } else {
        theString = [theString stringByReplacingCharactersInRange:range 
                                                       withString:string];	
    }
	
    CGFloat theValue = [theString floatValue];

    [self updateValue:theValue 
         forTextField:textField
           isClearing:NO];
    
    return YES;
}
     
-(BOOL)updateValue:(CGFloat)theValue 
      forTextField:(UITextField *)textField
        isClearing:(BOOL)clearing
{
    NSIndexPath *myIndexPath = [self indexPathForTextField:textField];
    Variable *var = [self.variableFRC objectAtIndexPath:myIndexPath];
    
    if (theValue != var.value) {
        if (clearing) {
            [self.model clearVariable:var];
        } else {
            [self.model updateVariable:var
                         withValue:theValue];
        }
        [self updateCellLabelAtIndexPath:myIndexPath
                                 toColor:[self.model variableNameColor:var]];
        return YES;
    }
    
    return NO;
}

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [self moveToNextTextField:textField];
    return YES;
}
As for the problem I'm trying to address, for cells that are already on the screen there isn't a problem. For cells that are not on the screen, I'm really not sure what to do, and I'm not sure I like my first thoughts.

If [self.tableView cellForRowAtIndexPath:] returns nil, that definitely means the cell is NOT on the screen. In that case, I can get a cell using tableView:cellForRowAtIndexPath: in the moveToPreviousTextField: method, it will come from the reuse pool. That's fine, of course, but I then seem to need to stash that aside until the tableView asks for the same cell. So I'll add two ivars - one for the locally-cached cell and another for the indexPath of that cell, and modify tableView:cellForRowAtIndexPath: to check this cache first and use that one.

Wow, this all seems like a lot of bookkeeping for something that would be better managed the tableView (IMHO). I'll give that a spin and see if it takes care of it.

BTW, my previous thought (scrolling before asking the tableView for the cell) didn't quite cut it, probably because it never made it back to the main loop...
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
As for the problem I'm trying to address, for cells that are already on the screen there isn't a problem. For cells that are not on the screen, I'm really not sure what to do, and I'm not sure I like my first thoughts.

If [self.tableView cellForRowAtIndexPath:] returns nil, that definitely means the cell is NOT on the screen. In that case, I can get a cell using tableView:cellForRowAtIndexPath: in the moveToPreviousTextField: method, it will come from the reuse pool. That's fine, of course, but I then seem to need to stash that aside until the tableView asks for the same cell. So I'll add two ivars - one for the locally-cached cell and another for the indexPath of that cell, and modify tableView:cellForRowAtIndexPath: to check this cache first and use that one.

Wow, this all seems like a lot of bookkeeping for something that would be better managed the tableView (IMHO). I'll give that a spin and see if it takes care of it.
Update: Not quite right... The caching definitely works as expected, mostly because it's simple, but trying to give the input focus to something that's not on the screen doesn't work - in fact, it pretty much does nothing. So it's on to plan C: instead of switching the input focus to this cell that's not on the screen, scroll it so that it is on the screen and when it's displayed set the input focus to the new textField in that cell.
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
Update: Not quite right... The caching definitely works as expected, mostly because it's simple, but trying to give the input focus to something that's not on the screen doesn't work - in fact, it pretty much does nothing. So it's on to plan C: instead of switching the input focus to this cell that's not on the screen, scroll it so that it is on the screen and when it's displayed set the input focus to the new textField in that cell.
SUCCESS! Plan C worked as expected. I added the code that checked if the cached cell needs the focus in tableView:willDisplayCell:forRowAtIndexPath: and if it does and the indexPath matches the cached indexPath then the focus is moved to the textField in the current cell.

Thanks everyone who helped. You might not think you did, but all of your comments helped me think this through and actually make it work.

This is one of a handful of last steps before I submit my (first!) app to the app store. When it gets approved, I'll follow up here.
 

PhoneyDeveloper

macrumors 68040
Sep 2, 2008
3,114
93
If you consider the selected row as part of your data model then saving that and then selecting the text field for that row when it scrolls on screen in cellForRowAtIndexPath is what I said. That's how table views work. Placing a checkmark on a cell when its tapped is similar.
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
Thanks for the reference. Turns out that I was already doing the scrolling part, using UITableViewScrollPositionNone which is pretty handy, but the principal difference is that the I was having trouble getting a reference to a UITextField for cells that are NOT visible (== not instantiated/nonexistent); the reference uses/refers to static cells where those cells already exist while I'm using dynamic cells (allocated via dequeueReusableCellWithIdentifier:).

Here's a screenshot of an exemplar:
Screen Shot 2012-06-06 at 7.30.13 AM.png

(this is the case that caused the problem I reported in the first post.)

If I tap the "Next" button it moves just fine to the next cell (which is due to the next cell existing - you can see it on the screen), but tapping "Previous" causes the problem I was fighting. The previous cell simply doesn't exist, and hence the textField in that cell doesn't exist either - it doesn't exist in the table view, it doesn't exist in this view controller, it just doesn't exist but it CAN exist if something asks for it. That's what I had to do here: ask for it and remember it for a little bit before the tableView asks for it again. I thought the tableView would ask for it but I was wrong, so I had to force the tableView to ask for it.

I'm not entirely thrilled with the amount of hassle this involves and I'd be surprised if this is what others are doing to make a previous button work. Now that it's working, perhaps I can get some thoughts about simplification. Am I making this harder than it needs to be? Turns out that textFields in table view cells is a pretty common pattern that I've been using and I've never really figured it out well enough - this is the first time I've made it work correctly.

This is the code that resulted. I've highlighted the new code to help the discussion.

Code:
@interface MCVariableTableViewController ()
[B]@property (strong,nonatomic) MCTextFieldCell *cachedCell;
@property (strong) NSIndexPath *cachedIndexPath;
@property (assign) BOOL cacheNeedsFocus;
[/B]@end

@implementation MCVariableTableViewController

[B]@synthesize cachedCell;
@synthesize cachedIndexPath;
@synthesize cacheNeedsFocus;
[/B]
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MCTextFieldCell *cell = nil;
    
[B]    if ([indexPath isEqual:self.cachedIndexPath]) {
        cell = self.cachedCell;
    }
[/B]    
    if (!cell) {
        cell = (MCTextFieldCell *)[tableView dequeueReusableCellWithIdentifier:@"MCTextFieldCell"];
    }
    
    if (!cell) {
        NSLog(@"cell == nil???");
        abort();
    }
    
    [cell prepareForReuse];
    return [self configureCell:cell
                   atIndexPath:indexPath];
}

[B]-(MCTextFieldCell *)cachedCell
{
    MCTextFieldCell *theCell = cachedCell;
    cachedCell = nil;
    return theCell;
}

-(void)cacheCell:(MCTextFieldCell *)cell
    forIndexPath:(NSIndexPath *)indexPath
{
    self.cachedCell = cell;
    self.cachedIndexPath = indexPath;
    self.cacheNeedsFocus = YES;
}
[/B]
-(void)     tableView:(UITableView *)tableView 
      willDisplayCell:(UITableViewCell *)cell
    forRowAtIndexPath:(NSIndexPath *)indexPath
{
    MCTextFieldCell *textFieldCell = (MCTextFieldCell *)cell;
    
    // Adjust label position, if necessary
    CGFloat labelHeight = [self labelHeightForRowAtIndexPath:indexPath];
    if (labelHeight > textFieldCell.label.bounds.size.height) {
        CGRect newFrame = textFieldCell.label.frame;
        newFrame.size.height = labelHeight;
        newFrame.origin.y = (cell.bounds.size.height - labelHeight)/2.0;
        textFieldCell.label.frame = newFrame;
    } 
    
[B]    if (self.cacheNeedsFocus && [indexPath isEqual:self.cachedIndexPath]) {
        self.cacheNeedsFocus = NO;
        self.cachedIndexPath = nil;
        [textFieldCell.textField becomeFirstResponder];
    }
[/B]
}

-(void)moveToNextTextField:(UITextField *)textField
{
    NSIndexPath *myIndexPath = [self indexPathForTextField:textField];
    NSIndexPath *nextTextFieldIndexPath = [self nextTextFieldIndexPathForIndexPath:myIndexPath];
    
[B]    [self moveFromTextField:textField
     toTextFieldAtIndexPath:nextTextFieldIndexPath];
[/B]}

-(void)moveFromTextField:(UITextField *)textField
  toTextFieldAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath) {
        MCTextFieldCell *newCell = (MCTextFieldCell *)[self.tableView cellForRowAtIndexPath:indexPath];
        
[B]        // newCell is nil if it's not already on the screen
        if (!newCell) {
            newCell = (MCTextFieldCell *)[self tableView:self.tableView
                cellForRowAtIndexPath:indexPath];
            [self cacheCell:newCell forIndexPath:indexPath];
            [self.tableView scrollToRowAtIndexPath:indexPath
                                  atScrollPosition:UITableViewScrollPositionNone
                                          animated:YES];
            self.currentTextField = newCell.textField;
        } else {
 [/B]           self.currentTextField = newCell.textField;
            [self.currentTextField becomeFirstResponder];
        }        
    } else {
        [textField resignFirstResponder];
        self.currentTextField = nil;
    }
    
}

-(void)moveToPreviousTextField:(UITextField *)textField
{
    NSIndexPath *myIndexPath = [self indexPathForTextField:textField];
    NSIndexPath *prevTextFieldIndexPath = [self previousTextFieldIndexPathForIndexPath:myIndexPath];
    
[B]    [self moveFromTextField:textField
     toTextFieldAtIndexPath:prevTextFieldIndexPath];
[/B]}
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
If you consider the selected row as part of your data model then saving that and then selecting the text field for that row when it scrolls on screen in cellForRowAtIndexPath is what I said. That's how table views work. Placing a checkmark on a cell when its tapped is similar.
I don't understand what you're suggesting. Are you suggesting that I store in the model which cell happens to have the input focus right now? That really doesn't make much sense to me.

My current/former issue is not about the content of the cell per se, rather it's about moving the input focus for text input among a bunch of textFields in separate UITableViewCells in a UITableView. This has nothing to do with the model - it's the view and controller that are interacting here. Take a look at my post that occurred after yours and see if that helps to explain some of the context of my issue.

Are you recommending that I send the previous/next indications to the model, and then have the model tell the view controller to scroll the tableview and then put the input focus into the now-scrolled-to cell?

Let me turn it around on you this way: what changes would you make to the code I posted (e.g., move this method to the model, add a protocol between the model and view controller to provide this indication, ...)? This request is not purely academic - this issue is problematic to me in several apps that I'm responsible for (internal to my company) where the same issue pops up, and I suspect others will have an issue with a similar scenario. I'm willing to make changes to the code if I can understand why those changes are being made.
 

PhoneyDeveloper

macrumors 68040
Sep 2, 2008
3,114
93
Are you suggesting that I store in the model which cell happens to have the input focus right now? That really doesn't make much sense to me.

Yes. You're already doing exactly this. You're storing the indexPath for the selected path. That's what I meant.

As a practical matter. Move the setFirstResponder to cellForRowAtIndexPath: So if the cell being returned is the selected cell then call setFirstResponder on its textField.

I don't see the point of the cachedCell code.

If you want to change the current focused cell call the table's cellForRowAtIndexPath:. If the cell exists you can set it to first responder, although you could just use your view controller's cellForRowAtIndexPath for this if you reload the row or reloadData. Obviously save the selected cell indexPath (or row if that's good enough) so it can be accessed in your View Controller's cellForRowAtIndexPath.
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
Thanks for the response. I'm not arguing with you, I'm discussing an alternate implementation that may be better than the one I currently have. As I noted above, I do appreciate your inputs, I'm just trying to understand what you're suggesting so I can do what I'm doing better. So have some patience with me and we'll get to a conclusion that will be better than where we started.

Given that, I'm going to continue the discussion (I like having this in public and politely because maybe others on the forum can see what such a discussion looks like).

Are you suggesting that I store in the model which cell happens to have the input focus right now? That really doesn't make much sense to me.

Yes. You're already doing exactly this. You're storing the indexPath for the selected path. That's what I meant.
I'm storing it in the view controller, not in the model, because I don't see it needing to persist past the view controller lifetime (ie., if the view controller disappears the input focus doesn't matter the next time it shows up) vs things like the value entered into the field which does need to be longer lived.

As a practical matter. Move the setFirstResponder to cellForRowAtIndexPath: So if the cell being returned is the selected cell then call setFirstResponder on its textField.
I'll give it a try and see what happens. The reason I put it into the tableView:willDisplayCell:AtIndexPath: method is that at that point the cell is already a subview of the tableView (maybe, I'll verify that), and therefore has a valid (and visible) frame. It CAN'T be a subview inside tableView:cellForRowAtIndexPath: because it didn't exist.

I'd rather do it where you recommend, I just don't think it will work. We'll see soon enough.
I don't see the point of the cachedCell code.
The ivar cachedCell is there to hold the cell that I created in the moveFromTextField:ToTextFieldAtIndexPath: method. It definitely seems that creating the cell at that point is unnecessary if I have the indexPath stored as well. I'll remove that and see what happens.

If you want to change the current focused cell call the table's cellForRowAtIndexPath:. If the cell exists you can set it to first responder, although you could just use your view controller's cellForRowAtIndexPath for this if you reload the row or reloadData. Obviously save the selected cell indexPath (or row if that's good enough) so it can be accessed in your View Controller's cellForRowAtIndexPath.
That's the problem I was having originally - see the very first posting. If you look at the code I have now, I call [self.tableView cellForRowAtIndexPath:indexPath]. If that returns a cell, I just make that cell's textField the first responder. If it returns nil, then I have to make the cell show up, which I do by scrolling the tableView. That then causes the tableView to ask the dataSource for a new cell using tableView:cellForRowAtIndexPath: to provide that new cell. It seems very likely that I don't need to create the cell until the tableView asks for it.

Ok, here's the net result of this part of the discussion:
a) remove cachedCell - it seems redundant.
b) leave in the cachedIndexPath for later use in either tableView:cellForRowAtIndexPath or tableView:willDisplayCell:atIndexPath:. I plan to leave this in the view controller because of the lifetime thing I mention above.
c) Test out setting the input focus (via setFirstResponder) in tableView:cellForRowAtIndexPath: instead of where it is now.

See, it was productive and polite, and I'll provide a response after I finish the changes.

----------

Ok, here's the net result of this part of the discussion:
a) remove cachedCell - it seems redundant.
b) leave in the cachedIndexPath for later use in either tableView:cellForRowAtIndexPath or tableView:willDisplayCell:atIndexPath:. I plan to leave this in the view controller because of the lifetime thing I mention above.
c) Test out setting the input focus (via setFirstResponder) in tableView:cellForRowAtIndexPath: instead of where it is now.
Ok, I've done a) and b) and it works fine. I like this better, 1 property instead of 3 and simpler code all around, so I'm keeping it.

Next thing to do is try out c) as well as verify my claim of subview-ness of the cell.

----------

I tried c) and dog-gone-it if it didn't just plain work. I'm actually a little surprised.

Since it makes more sense to do this in the c) way, I'm going to leave it there for the same reason as the other changes - the code is simpler and clearer.

As for subview-ness, the cell is definitely a subview of the tableView in tableView:willDisplayCell:atIndexPath: but not in tableView:cellForRowAtIndexPath:. However, that doesn't seem to matter like I thought it did/would.

Thanks for the help (even if you don't think you did that much).
 
Last edited:

PhoneyDeveloper

macrumors 68040
Sep 2, 2008
3,114
93
I'm storing it in the view controller, not in the model
Same same. ivars in your view controller are model objects.

I tried c) and dog-gone-it if it didn't just plain work. I'm actually a little surprised.
Well zippidty-do-da. I haven't had so much fun since the pig ate my little sister ;-)

Anyway, so all my suggestions worked and your code is better? Kewl. As it happens I wrote some code this week that was similar to this so it was fresh in my head. Textfields do some weird and wonderful things in tableviews.

Do you resize the table view when the keyboard appears?
 

Ron C

macrumors member
Original poster
Jul 18, 2008
61
0
Chicago-area
Do you resize the table view when the keyboard appears?
No, mostly because I don't seem to need to. I think it's because my view controller is a subclass of UITableViewController, and I've read several times how this is managed pretty well by the base class.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.