I have a UITableViewController with prototype cells containing UITextFields. To configure these custome cells, I've created a UITableViewCell subclass. I've conected the textField to the cell subclass via an outlet (nonatomic, weak).
On this subclass I've created a protocol for which the UITableViewController is its delegate so that everytime something changes in these textFields, the TableViewController knows about it. Basically I wanted this to save the values on the NSUserDefaults
Besides, in order to dynamically obtain values from these textFields, I can do something like this:
((TextFieldCell*)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]).textField.text
It works ok most of the times. However when the textField is outside of the view because it has scrolled, the vaulue I get from textField.text is (null). As soon as it gets in the view again, everything goes back to normal.
I tried to change the outlet from weak to strong but to no avail.
I guess I could define some private NSStrings on the class, and fill them out when the delegate protocol gets called. The thing is that I wanted to get my code as generic as possible, keeping the need for private variables as low as possible, mostly to simplify the cell generation code.
Is there any other way to get the values of the textFields when they are outside of the view?
Thanks in advance!
But you know that UITableView only keeps Cells for the visible rect?
When a cell leaves the screen, and a new cell is needed for another cell moving into the visible area, the old cell is reused for the new content.
So there is not one cell for each row of your table view.
And if your table contains a lot data, there are far more rows than cells.
As Thyraz said, the UITableView only keeps cells for the visible rect -- and a reasonable buffer to allow for scrolling. Thats why 'reuse identifiers' are so very important, they indicate which cells can be used for which tables (critical when you have more than one table to worry about). Unfortunately, that doesn't answer your question by itself.
The responsibility for storing the contents of those textViews isn't on the UITableView's shoulders. It's your job to provide that data through the data source delegate protocols, and therefore you should be querying the data source for that information.
Edit: Which means that yes, you should be storing this data somewhere else, usually in the form of properties on the view controller class that contains the table view. I'd recommend the use of NSArray for the purpose, but you can also do it through dicts or even, at the last resort (and this is more a in theory you can do this, but it's an incredibly bad idea kind of thing), a series of properties. Personally, I almost always use NSArrays because they're structured in a manner appropriate to the problem, but you could theoretically do it other ways. (I've used a dict based structure exactly once, and that was a situation where my data was nested inside itself in a recursive structure)
UITableViewController doesn't keep cells around once off the screen. You can use the following pattern to get a previously used one as a memory management optimization, but you MUST assume that cells need to have the values reset on them every time they come onto the screen (even if dequeued) because there is no guarantee what the values will be.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier1 = #"Cell1";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier2];
if( cell == nil ) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier1] autorelease];
cell2.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell2.editingAccessoryType = UITableViewCellAccessoryNone;
}
switch( indexPath.section ) {
case first_Section:
if( row == 0 ) {
cell1.textLabel.text = #"Some Text";
cell1.accessoryView = [self myCustomViewControl];
cell = cell1;
}
... etc
}
}
Related
I use the codes below to add textfield to each tableview cell,
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger row=[indexPath row];
static NSString *SimpleTableIdentifier1 = #"CellTableIdentifier";
//if I change the code to [NSString *SimpleTableIdentifier1 =NSString stringWithFormat:#"CellTableIdentifier%d",row]; everything is fine
UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier: SimpleTableIdentifier1 ];
if (cell == nil){
CGRect cellframe=CGRectMake(0, 0, 200, 60);
cell=[[[UITableViewCell alloc] initWithFrame: cellframe reuseIdentifier:SimpleTableIdentifier1] autorelease];
UITextField * textfieldCell =[[UITextField alloc]init];
textfieldCell.frame = CGRectMake(100.0f,20.0f,60.0f,26.0f) ;
[textfieldCell setDelegate:self];
[textfieldCell setTag:40000+row];//add row value here for later use,
[cell.contentView addSubview:textfieldCell];
[textfieldCell release];
}
UITextField *textfieldCell ;
textfieldCell =(UITextField*)[cell.contentView viewWithTag:40000+row];
textfieldCell.text=[ NSMutableString stringWithString:#"aaa1"];
DebugLog(#"---------%#",textfieldCell.text);
return cell;
}
textfieldCell.text sometimes displays null rather than my expectation value of 'aaa1'
this means that the code line at:
textfieldCell =(UITextField*)[cell.contentView viewWithTag:40000+row];
sometimes returns nil, try to fix this confused result but failed
Your comment welcome
This dequeueReusableCellWithIdentifier
UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier: SimpleTableIdentifier1 ];
returns a non nil cell Docs
This method dequeues an existing cell if one is available or creates a new one using the class or nib file you previously registered. If no cell is available for reuse and you did not register a class or nib file, this method returns nil
that may has a textField with different tag resulting in nil for
UITextField *textfieldCell ;
as these 2 lines
// here rhs may be nil
textfieldCell =(UITextField*)[cell.contentView viewWithTag:40000+row];
textfieldCell.text=[ NSMutableString stringWithString:#"aaa1"];
has no effect with nil textfield , also it's shocking how you still work with MRC ( Manual reference counting ) here
[textfieldCell release];
please update to ARC ( Automatic reference counting ) which will removes your worries about memory management issues
UITableView uses a dynamic pool of cells for performance reasons. The only cells which are actually active are those which are currently displayed on the screen. Any cells which are scrolled out of view are removed from the view and returned to a pool identified by, in your case, "SimpleTableIdentifier1"
The method dequeueReusableCell retrieves a cell from the pool, if one is available, or creates one if a class or XIB has been registered, otherwise returns nil.
The upshot of this is that your code may return a previously created cell which has been used for some other row earlier. The tag which exists for that cell will most likely relate to some other row. This will all depend on the scrolling of rows on and off screen.
Changing the identifier to "CellTableIdentifier"%row causes each cell to be allocated from its own pool and avoids this conflict. However that will completely defeat the purpose of reusing cells.
Since you are using tag just to locate the text field within the cell you do not need to make the tag unique across all cells of the table. Just use a simple constant ie. 4000 not 4000 + row. viewWithTag will return the view within the hierarchy which matches, and you are starting at the content view anyway.
Having said all that, the structure of your code is really not ideal. The better approach is to define your cell as a class, the layout in the storyboard, and access the text field directly as a member of the class.
I have UITableView which cells contain one UITextField in it. My UITableViewController is a delegate of all this text fields. Now, when UITableViewController gets deallocated, I want to set delegate of all that text fields to nil. The text field has a tag, so I can get it by it's tag once I have a cell.
The question is how to get all created cells? Asking UITableView for visibleCells returns only visible cells, but it can happen, that there is a row which is not visible, bit it still has my UIViewController as a delegate. So I really need to get all created cells somehow. cellForRowAtIndexPath does the same, so it wouldn't work for me either. The only way I see here is to store all text fields in array, but may be there is a better way?
Here is some code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = (UITableViewCell*)[tableView dequeueReusableCellWithIdentifier:#"reuseId"];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"reuseId"];
UITextField *nameTextField = [[UITextField alloc] initWithFrame:nameTextFieldRect];
nameTextField.tag = TEXT_FIELD_TAG;
nameTextField.delegate = self;
[cell.contentView addSubview:nameTextField];
}
return cell;
}
-(void)dealloc
{
// todo: get all text fields and set theirs delegate to nil
}
Well, most answers suggest that I don't need to set delegate to nil, but as I'm paranoid, I suspect that the following scenario is possible:
User taps 'Back' button, so dealloc of my view controller is called. In dealloc my view controller releases it's table view, but the tableView still exists at this point, as well as all the text fields. And if somehow one of text fields would call it's delegate method at this point, the app would crash, because the delegate is not a valid object anymore.
If someone can explain why this scenario is not possible, than it would convince me that I don't need to set delegate to nil.
You do not need to do that. All of the cells will be deallocated as well, so they won't have a reference to the delegate.
By default the delegate is a weak reference, so it will not retain your object.
I am not expecting to have this answer marked as accepted, but this won't fit into a comment window.
So, rather than us trying to convince you, you could start one of the most important tool for an iOS developer, which is the profiler. And see for yourself by playing with the interface that you should get no more than the number of cells necessary to fill the screen are kept allocated, and then when you tap back, all are getting deallocated.
If they are not, they probably have a strong reference to something else, but this can easily detected with the profiler.
I also like to add that when working with cells the act of scrolling UITable, tap back enter again into table, scroll tap back (repeated at least 10 times) it's a mandatory practice to detect memory leak.
Also, I don't know the purpose of assigning a tag to the cell, I maybe wrong but with this:
nameTextField.tag = TEXT_FIELD_TAG;
consider that you have more than one cell with the same tag, therefore you cannot simply recall the desired one. I remember that the rule is the first placed on screen 'win' the TAG (or kind of).
[UPDATE]
Just a guess, I have never proved this, but to stay on the question, if your problem is to have a cell first for having the UITextView, have you tried to loop the main view and just ignore the cell:
UITextView textView = [mainView viewWithTag:TEXT_FIELD_TAG];
while(textView!=nil){ // or whatever loop or criteria you like
// deallo, nil, etc...
textView = [mainView viewWithTag: TEXT_FIELD_TAG];
}
Create a new delegate/protocol for that cell, and implement that delegate in the view controller, like PersonTableViewCellDelegate. Apart from that, your cell implements the UITextField delegate and in that code, call [self.delegate onKeyPressed] or whatever. I recommend you pass also a reference of the cell, so in the view controller you can use indexPathFromCell (or something like that) to know the position of the cell. If you are interested tell me about it and will copy some code.
I have a custom UITableViewCell. When a cell gets selected, a UILabel gets added to it. I had to use prepareForReuse for it not to get messy, like so:
- (void)prepareForReuse {
NSArray *viewsToRemove = [self.view subviews];
for (UILablel *label in viewsToRemove) {
[label removeFromSuperview];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CategorieCell *customCell = [tableView dequeueReusableCellWithIdentifier:#"cellID" forIndexPath:indexPath];
return customCell;
}
The problem is when I scroll down enough that the label is out of view, and then I scroll back up, the label isn't there anymore. The reason is obviously because when the cells get reused, I removed all the labels.
So is there a way to disable prepareForReuse (or just the code in the method) for the selected row, and how?
Cells that are scrolled away will be reused, and there's no way around it. Even if you avoid the removeFromSuperview logic, that cell will reappear at a different index path, probably not where you want it.
The way to conditionally configure cells is in cellForRowAtIndexPath. There, you can ask if the indexPath is among the table view's indexPathsOfSelectedCells. If it is, then configure it with the extra labels, or not, if not.
One way to reduce the messiness is to have those labels remain in the cell unconditionally, just setting their alphas to 0 or 1, depending on the selection state.
For example, in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
// if you know the table has singular selection
NSIndexPath *selectedIndexPath = [tableView indexPathForSelectedRow];
BOOL rowIsSelected = [indexPath isEqual:selectedIndexPath];
// OR, for multiple select...
NSArray *selection = [tableView indexPathsForSelectedRows];
BOOL rowIsSelected = [selection containsObject:indexPath];
// now either conditionally create/destroy or show/hide the subviews
// that appear on selection (I prefer show/hide for simpler cells)...
[cell configAsSelected:rowIsSelected]; // have the custom cell do it
// in that method, or here, if you're less OO-inclined...
cell.subviewThatAppearsOnSelected.alpha = (rowIsSelected)? 1.0 : 0.0;
The larger point is, this is the suggested place to reliably configure a cell based on the model and its current position in the table
Think of table cells as dumb containers that get reused to hold different things (labels, images, buttons, etc.).
You fill the cells in cellForRowAtIndexPath.
You empty them in prepareForReuse so they can be filled again and reused.
Don't confuse these two actions.
When you fill the cell, you should be filling it from data that you have stored somewhere else - i.e. not from other cells. If you are relying on indexPathsOfSelectedCells to help you when filling your cell, you are going to have problems. Don't do this.
Typically you would have an array of objects, where each object corresponds to a cell. You have as many cells in your table as objects in the array. You might initialize the objects in your array in viewDidLoad, or pass them in from a previous view controller.
This process doesn't have to be complicated. Most cells display only a few bits of data, so your object (often called a model) doesn't have to have many properties to hold this data.
When the user selects a cell, set a "selected" property in its corresponding object to indicate this. This value stays around in the object even when the cell is scrolled off the screen and reused. That's good.
Now when the user scrolls back to the cell, you fill the cell with data from the corresponding object. Since that object has its "selected" property set, you "fill" the cell by adding the label that you want there in this case. Or if it isn't set, you don't add the label.
In prepareForReuse, always remove the label to put the cell in its empty state, ready to be refilled.
I am working on an ios application,
I have a normal table view. When calling heightForRowAtIndexPath I am doing the folowing
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellId = [self getCellIds][indexPath.row];
BaseTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
return [cell calculateHeigh];
}
Basically I am dequeueing the cell because I have a function calculateHeigh inside every cell that will do the height calculation. this is working fine as intended however I have a concern:
Is it safe to call dequeueReusableCellWithIdentifier: inside the heightForRowAtIndexPath ? will it cause any issue?
EDIT:
Just to clarify why I did this, I have a big amount of custom cells with different identifier that needs to be loaded. and to avoid having a huge if-else statement in my heightForRowAtIndexPath I placed the getter of the cell height in the custom cell that way I just ask it to return it (no calculation is made there), I can't do it as a class method as I don't know which class, I can get the object from the identifier and not the class. And I want to avoid a big if-else just for code readability.
So my concern was with the dequeueReusableCellWithIdentifier: is it heavy to call it when getting the height? will it cause memory issues or lags? or is it worth to just do a bug if-else of use a dictionary?
First of all you should avoid any calculations in table drawing methods(such as heightForRow, cellForRow, etc). These methods are called a lot and although your table may be short and/or not complicated(with custom cells with a lot of labels, buttons and images) you should always try to optimize this drawing process or otherwise user will experience some nasty lag when scrolling.
So you should call some method to prepare data before calling 'reloadTable'
-(void)prepareMethod
{
//get only one cell to calculate all row heights
BaseTableViewCell *cell = [_myTableView dequeueReusableCellWithIdentifier:cellId];
for (NSDictionary* dataObj in _dataArray)
{
//loop through all rows data and set new property for row height
dataObj[#"rowHeight"] = [cell calculateHeigh];
}
}
And then when calling heightForRow just pass this value without any expensive operation(such as probably string calculations):
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//always make sure you don't access unexisting array index
return ( indexPath.row < _dataArray.count ) ? _dataArray[indexPath.row][indexPath.row][#"rowHeight"] : 1.0;
}
Of course you don't need separate method just to populate row heights in your data array - you can populate this value when populating(formatting) your data array to avoid second array iteration. It all depends on your current implementation.
Just remember that expensive drawing methods(not only for table though) should always be as short as possible and just get data needed for drawing and draw. It's really so simple. If you need to make some complicated calculations do it before that(maybe in view init) so your data is prepared before actual drawing. This way your application will be working smoothly even with bigger tables(because no matter how big the table is, UITableViewController draws only visible cells).
Regards,
hris.to
I don't like to have big if statements in heightForRowAtIndexPath and accessing a cell using dequeueReusableCellWithIdentifier. Your approach getting cell height from each cell quite is reasonable. I believe your calculateHeigh return value depends on the table data you pass into the cell.
In BaseTableViewCell.h
+ (CGFloat)heightWithData:(id)data;
In BaseTableViewCell.m
+ (CGFloat)heightWithData:(id)data
{
//put your calculateHeigh logic here. I believe your calculateHeigh depends on the data each cell has.
}
Then you can do
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [BaseTableViewCell heightWithData:[self.tableData objectAtIndex:indexPath.row]];
}
If you do this, you don't need to access each cell object to get cell height.
You should not use this method to provide the calculation. Based on what I can see on your setup, you are calculating the height based on the values already on your cell. What happens is that the cell dequeue system will give you a cell to reuse, but because it's sharing cells from multiple index paths, that cell probably has data that belongs to a record of an index path different from the current one. Get the calculate height code and try to reproduce it inside the datasource callback you are using.
I have a UITableview cell that gets a tally from a core data database. The tallyTable is in a view controller inside a UITab view. I have an NSLog statement that prints out the tally value whenever it gets updated. Another tab has a list to change the source (different day) for the tallies. I am using iOS5 with ARC targeting iOS 4.2.
Here's the problem. When I load the application, the correct tallies for whatever the last selected day show up in the table tab. If I then go to the day tab and change the day and return to the tally tab there is no change in the display. However, the viewWillAppear on the tally tab runs and as the table cycles through cellForIndexPath, my NSLog statement prints out all the correct new values. If I then scroll the top label off the screen and back the label updates to the new value.
I've tried setNeedsLayout and setNeedsDisplay on the UILabel, the UITableViewCell, the UITableView and the view controller loading the table. I tried changing the CellReuse identifier so that it would never reuse a cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CollectionItemTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CollectionItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSUInteger row = [indexPath row];
cell.textLabel.text = [[self.collectionKeys objectAtIndex:row] valueForKey:#"collectionTitle"];
NSInteger test1 = indexPath.row + 150;
NSLog(#"tag = %i", test1);
cell.tallyButton.tag = test1;
NSNumber * questionID = [[self.collectionKeys objectAtIndex:row] valueForKey:#"answerID"];
cell.tallyLabel.text = [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]];
NSLog(#"Collection text should be = %#", [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]]);
[cell setNeedsLayout];
return cell;
}
I've read over a half dozen other similar questions. Got about three hours invested so far in trying to solve this.
EDIT: I thought I fixed it by using the navigation controller to repush the top level view controller on to the view again. I'll admit now this feels like a classically kludgy hack in every way. When the view is PUSHED everything updates and it is seamless. However, in order to have a fixed footer to make selection settings for the table buttons, I used a UIView with two subviews, a UITableView on top and a simple UIView with four buttons below.
The captions on the buttons need to change with the data source. Now when the view controller is pushed onto the view it obscures my fixed footer view. So, I inserted the fixed footer into the UITableview and everything appeared fine until I scrolled the UITableView and the footer scrolled up with it. The table is basically a tally sheet with buttons next to each item and in the footer is four buttons to note the color of the tallied item. Say the next item was a green lego, you would tap "green" in the footer and the button next to "lego" in the table. When I push the view controller with the two subviews the UITableview labels do not update. Thus the tableview needs to be pushed itself (as far as I can tell).
ANSWER: see comment below but ultimately I needed to reload both the visible UITableView data and the delegate UITableView controller data behind it.
I'll give it a shot. First, are you using ARC? If not, you need to add autorelease when you alloc/init a new cell. Otherwise, it's fine as is.
If I'm understanding your question correctly:
The tableView displays the correct data at app launch
You switch away from the tab with the tableView and change the tableView dataSource
You switch back to the tab with the tableView and you see (via NSLog) that the table cells are reloaded with the correct data yet the old data is still visible in the cells
If you scroll a cell off the display and back forcing it to refresh it contains the correct data
Some thoughts:
the tableView will not reload itself automatically when it's view appears. You need to call [tableView reloadData] whenever the dataSource changes. This is independent of whether the tableView is currently displayed or not. My guess is this alone will solve your problem.
You don't need to call setNeedsLayout on the cell unless you want the cell to relayout its subviews based on the data. You also don't need setNeedsDisplay.
I'm assuming there aren't other complicating factors (such as multiple tableViews displaying the same data) that could confuse things.
If you use prepare for reuse method, remember to over the original method with [super prepareForReuse];
Another method if the above way does not work is re setup cell as describe here.
I use the same method i applied for some of my collection view : we should remove/reset your subview where you create/add it to cell 's content. That mean we need set up data each cell completely for each row.
I move the code reset data value from prepare reuse to where i set value and I worked simply !
In my CustomCell.m :
- (void)configCellWith:(id)item At:(NSUInteger)row {
if (_scrollView) {
[[_scrollView subviews]
makeObjectsPerformSelector:#selector(removeFromSuperview)];
_scrollView = nil;
[_scrollView removeFromSuperview];
}
else {
CGFloat y = labelHeight+15;
float scrollHeight = _imgArray.count*200;
_scrollView=[[UIScrollView alloc]initWithFrame:CGRectMake(10, y,SCREEN_WIDTH-20, scrollHeight)];
_scrollView.scrollEnabled=YES;
_scrollView.userInteractionEnabled=YES;
_scrollView.layer.masksToBounds = YES;
[self.contentView addSubview:_scrollView]; } }
Remember to change your data source appropriately too.