OK, I’ve got a table with 3 different prototype cells (cellVaccination, cellAdmin, cellExpire). In my cellForRowAtIndexPath method, I’m splitting up a Core Data object across those 3 individual cells so that structurally the table will look like the following:
- Drug 1
- Drug 1 Admin
- Drug 1 Expire
- Drug 2
- Drug 2 Admin
- Drug 2 Expire
- Drug 3
- Drug 3 Admin
- Drug 3 Expire
Additionally, I’ve programmatically added a UISwitch into the ‘top level’ cell (i.e. Drug 1) so that the switch might control the secondary cells features (i.e. color, text, etc). Here is what my current cellForRowAtIndexPath looks like:
- (VaccineTableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// we need to adjust the indexPath because we split a single core data object into 3 different rows
NSIndexPath *adjustedIndexPath = [NSIndexPath indexPathForRow:indexPath.row / 3 inSection:indexPath.section];
Vaccine *vaccine = [self.fetchedResultsController objectAtIndexPath:adjustedIndexPath];
// define the switch that will get added to the primary table rows
UISwitch *switchview = [[UISwitch alloc] initWithFrame:CGRectZero];
if (indexPath.row % 3 == 0) {
static NSString *cellIdentifier = #"cellVaccination";
VaccineTableViewCell *cell = [myTableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
cell.vaccineName.text = vaccine.vaccineName;
// add a switch into that table row
cell.accessoryView = switchview;
[switchview addTarget:self action:#selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
switchview.tag = indexPath.row;
switchview.on = [vaccine.vaccineEnabled boolValue];
// PROBLEM AREA BELOW
if (switchview.on) {
VaccineTableViewCell *cell1 = [myTableView dequeueReusableCellWithIdentifier:#"cellAdmin" forIndexPath:[NSIndexPath indexPathForItem:indexPath.row + 1 inSection:0]];
cell1.vaccineAdmin.textColor = [UIColor redColor];
cell1.vaccineAdminDate.textColor = [UIColor redColor];
NSLog(#"Row %d is %#", indexPath.row, switchview.on ? #"ON" : #"OFF");
} else {
VaccineTableViewCell *cell1 = [myTableView dequeueReusableCellWithIdentifier:#"cellAdmin" forIndexPath:[NSIndexPath indexPathForItem:indexPath.row + 1 inSection:0]];
cell1.vaccineAdmin.textColor = [UIColor lightGrayColor];
cell1.vaccineAdminDate.textColor = [UIColor lightGrayColor];
NSLog(#"Row %d is %#", indexPath.row, switchview.on ? #"ON" : #"OFF");
}
//
return cell;
}
else if (indexPath.row % 3 == 1) {
VaccineTableViewCell *cell = [myTableView dequeueReusableCellWithIdentifier:#"cellAdmin" forIndexPath:indexPath];
cell.vaccineAdminDate.text = vaccine.vaccineAdmin;
return cell;
}
else if (indexPath.row % 3 == 2) {
VaccineTableViewCell *cell = [myTableView dequeueReusableCellWithIdentifier:#"cellExpire" forIndexPath:indexPath];
cell.vaccineExpireDate.text = vaccine.vaccineExpire;
return cell;
}
else {
// do nothing at the moment
}
}
The problem I’m having seems to stem around the area notated within the “Problem Area Below” element, more specifically I’m guessing with the dequeueReusableCellWithIdentifier. In theory, what’s supposed to happen is that when the cells are first populated via the Core Data objects, I want to test whether or not the switch is either “on” or “off” and adjust a parameter (such as color) appropriately so that, without any other interaction, the respective rows are colored appropriately.
What’s happening is this - let’s assume that I’m simulating on an iPhone 4S and that the screen is displaying 4 row sets, or 12 rows total (4 rows of 3 different prototypes). And let’s also assume that the first 2 are switched ON and the second 2 are switched OFF, again driven directly from Core Data. Initially, the screen will look correct, the first two items have been colored red, and the next two items have been colored gray. However when I start scrolling my table up, the NEXT two (that were off the screen) are colored red, and so the pattern continues. Oddly, when NSLog returns the Row identifiers (seen within that “problem area” section) everything looks like it’s identifying the correct rows, but apparently it’s not, i.e.:
vaccinations[10952:1486529] Row 0 is ON
vaccinations[10952:1486529] Row 3 is ON
vaccinations[10952:1486529] Row 6 is OFF
vaccinations[10952:1486529] Row 9 is OFF
vaccinations[10952:1486529] Row 12 is OFF
vaccinations[10952:1486529] Row 15 is OFF
I believe it has something to do with the dequeueReusableCellWithIdentifier method, however why would the NSLog identify the rows correctly, but the changing of the colors not hit the correct rows?
You have references to cell1 in which you dequeue a cell for a different NSIndexPath, configure the color of that cell, and then discard this cell. I'm guessing that you are trying to adjust the visual appearance of a different cell (the next cell).
That is not correct. The cellForRowAtIndexPath should be adjusting the state of the current cell only. If you want to adjust the appearance of cellAdmin cell, you should do that within the if (indexPath.row % 3 == 1) ... block.
So the if (indexPath.row % 3 == 0) block will look up in the model to determine if the switch is on or off. The if (indexPath.row % 3 == 1) block will look up in the model to determine what color the text should be.
But cellForRowAtIndexPath should not be trying to adjust the appearance of another cell. You have no assurances of what order these will be instantiated (and it may vary depending upon whether your scrolling, from which direction, etc.).
If one did want to update another cell that is visible, dequeueReusableCellWithIdentifier is not the correct method, regardless. Instead, one would use [tableView cellForRowAtIndexPath:] which retrieves the cell for a currently visible cell (and must not to be confused with the similarly named UITableViewDataSource method). But you would never do that in this context because you don't know if that other cell had been loaded or not. (I actually think it's a bad practice in general to update another cell in any context, a violation of the separation of responsibilities.)
Related
I want to make cells overlapping.
What I did is
Adjust the tableview's contentSize:
int count = [self.tableView numberOfRowsInSection:0];
self.tableView.contentSize = CGSizeMake(self.view.frame.size.width ,(100 * count - 30 * (count - 1)));
And set the cellForRowAtIndexPath :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"];
cell.contentView.backgroundColor = [UIColor orangeColor];
}
if (indexPath.row != 0) {
UITableViewCell *currentCell = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row + 1 inSection:0]];
[currentCell setFrame:CGRectMake(0, - 30, self.view.frame.size.width, 100)];
}
return cell;
}
But when I test, this is not working.
Any help? Thanks.
What is happing with your code is that the values that you are manually setting inside tableView:cellForRowAtIndexPath: are getting overwritten by the UITableViewController layout methods.
You are not supposed to edit the frame of a table view cell directly since this is an attribute that is set by UITableViewController internally. You should only interact with this by using methods of the UITableViewDelegate protocol. (tableView:heightForRowAtIndexPath:, ...)
IMHO the best way (cleanest one) to implement what you are describing is to subclass UICollectionView instead of UITableView and define a custom layout.
However if you wanna go the hacky way you can achieve an apparent row overlapping in many different ways.
Just as an example, you could create two different row types, one with height 30 and one with height 70, where the first one represents the overlapping part of the row and the second one represents the not-overlapping part of the row. Then use the first type for even rows and the second one for odd rows.
Hope this helps.
p.s. i'm really sorry if my english is not the best
I have been wondering why my code works well with cellForItemAtIndexPath: & not with dequeueReusableCellWithReuseIdentifier: while fetching collection view cells.
Here is my code:
This one works Fine:
NSInteger numberOfCells = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < numberOfCells; i++) {
myCustomCollectionCell *cell = (myCustomCollectionCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
//here I use the cell..
}
While this compiles well, but not working (the changes I perform on cell is not depicted)
NSInteger numberOfCells = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < numberOfCells; i++) {
myCustomCollectionCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"myCell"forIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
//here I use the cell..
}
Tried this too, but no use:
NSInteger numberOfCells = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < numberOfCells; i++) {
myCustomCollectionCell *cell = (myCustomCollectionCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:#"myCell"forIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
//here I use the cell..
}
Any ideas?
These two are basically two very different methods.
dequeReusableCellWithReuseIdentifier:
Suppose that you have a list of articles to view. Say you have 50 articles. The screen won't show you all 50 articles on screen at once. It will show you limited cells at once, based on the height you given to the rows. Lets say the screen shows only 5 articles at once and now you are at the top of the list. The list will show items 1-5. Now when you scroll, inorder to display the 6th item, the list reuses your 1st cell, configures it for the 6th one and display it. By this time your 1st cell is out of the view.
2.cellForRowAtIndexPath :
On the other hand cellForRowAtIndexPath returns you the cell which is already in the view or from IndexPath you provide. In this case if the cell is already in the memory, it will just return that or it will configure a new cell and return.
The example below is for UITableViews, but UICollectionViews can be handled in the same way.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
/*
* This is an important bit, it asks the table view if it has any available cells
* already created which it is not using (if they are offscreen), so that it can
* reuse them (saving the time of alloc/init/load from xib a new cell ).
* The identifier is there to differentiate between different types of cells
* (you can display different types of cells in the same table view)
*/
UITableViewCell *cell = [tableView dequeueReusableCellWithReuseIdentifier:#"MyIdentifier"];
/*
* If the cell is nil it means no cell was available for reuse and that we should
* create a new one.
*/
if (cell == nil) {
/*
* Actually create a new cell (with an identifier so that it can be dequeued).
*/
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"MyIdentifier"] autorelease];
}
/*
* Now that we have a cell we can configure it to display the data corresponding to
* this row/section
*/
//Configure the cell here..
/* Now that the cell is configured we return it to the table view so that it can display it */
return cell;
}
Let me know if you were still unclear.
They are different things:
cellForItemAtIndexPath gets an already-populated cell from the collection view.
dequeueReusableCellWithReuseIdentifier will possibly return a cell that can be re-used, when populating a new cell. It's designed to help reduce the number of cell objects in existence. If it fails (and it often will) then a new cell must be created explicitly.
I'm getting some weird behavior in my UITableViewController.
I've subclassed UITableViewCell and created by own "visited" property within it.
- (void)setVisited:(BOOL)visited animated:(BOOL)animated
{
[self setVisited:visited];
...
}
I set this property when I create the cell in tableView:cellForRowAtIndexPath: (the only place I create it) like below:
if (cell == nil) {
cell = [[ArticleListViewCell alloc] initWithReuseIdentifier:CellIdentifier art:art index:indexPath.row];
[cell setVisited:NO animated:NO];
}
Later, in tableView:didSelectRowAtIndexPath:, I set this property to YES:
ArticleListViewCell *cell = (ArticleListViewCell *) [tableView cellForRowAtIndexPath:indexPath];
[cell setVisited:YES animated:NO];
However, when I select a cell and then return to this UITableView, which currently has 10 cells, I find that not only has the cell I selected become "visited", but also another cell has as well. It's hard to explain, but if I select the 1st cell, the 7th also becomes visited, if I select the 2nd, the 8th also becomes visited, and so on. Does anyone know why this is, and how I should go about fixing it?
I've checked this question, but it doesn't seem to help much.
This is caused by cell reuse. You need to set visited every time, not just when you create the cell.
if (cell == nil) {
cell = [[ArticleListViewCell alloc] initWithReuseIdentifier:CellIdentifier art:art index:indexPath.row];
}
BOOL visited = ... // value for this indexPath
[cell setVisited:visited animated:NO];
And in your didSelectRow method you need to update some sort of data structure keeping track of which row has been visited. You use this data to set the value properly in the cellForRow method.
Do not use the cell to keep track of state. The cell is a view. Your data source needs to track the state.
I have a NSTimer that calls this method every fourth second:
- (void)timerDecrement
{
timerCount = timerCount-1;
[OtherViewControllerAccess updateTimeLeftLabel];
}
In the updateTimeLeftLabel in the other class:
- (void)updateTimeLeftLabel
{
int timeLeft = OtherClassAccess.timerCount;
UILabel *timeLeftLabel = [[UILabel alloc] initWithFrame:CGRectMake(200, 10, 120, 20)];
timeLeftLabel.text = [NSString stringWithFormat:#"Tid kvar: %ih", timeLeft];
[cell addSubview:timeLeftLabel];
}
Basically I want my app to update a label in a cell in the tableview with the current time left, but the above method doesn't do anything to the call. So my question is, how can I add this subview to the cell outside the cellForRowAtIndexPath:delegate method, and then make it update that label every time the method is called.
So my question is, how can I add this subview to the cell outside the
cellForRowAtIndexPath:delegate method, and then make it update that
label every time the method is called.
The answer is, don't add subviews to a table view cell outside of cellForRowAtIndexPath. The cells belong to the table view, and you absolutely, categorically, should NOT try to modify them. That's the table view's job.
Just as a small example of what's wrong with your code, you would be adding an ever-increasing number of label views to your table view cell, one every 1/4 second. That's bad.
Second point: Which cell is "cell"? A table view manages a whole table of cells. If the user scrolls, some cells are scrolled off-screen and replaced with different cells.
Instead, you should figure out which indexPath contains the cell with your data in it, change the data in your model, and tell your table view to update the cell at the appropriate indexPath. That will cause it to redraw with updated contents.
Here is how I did something similar. I created a custom UITableViewCell class that has a timestamp UILabel:
#property (nonatomic) UILabel *labelTimestamp;
In that cell's layoutSubviews, I update the label size based on its title.
- (void)layoutSubviews {
[super layoutSubviews];
[self.labelTimestamp sizeToFit];
...
}
I then have an NSTimer firing every minute in my UIViewController that update that label in every visible cell (you could adapt to update only one cell with a specific indexPath).
- (void)timerDidFire
{
NSArray *visibleCells = [self.tableView visibleCells];
for (GroupViewCell *cell in visibleCells) {
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
[cell.labelTimestamp setText:[self.groupController statusUpdateDateAtIndexPath:indexPath]];
[cell setNeedsLayout];
}
}
I would keep the set up of the cell's centralized in cellForRowAtIndexPath: method. You can keep using your NSTimer to call reloadRowsAtIndexPaths:withRowAnimation: with the indexes of the cell/cells you want to update and therefore cellForRowAtIndexPath: will be called again.
Why don't you put that value (which you want to display in the cell) in a variable and assign a UILabel that value. In your updateTimeLeftLabel just call [self.tableView reloadData].
You can reload whole table or some specific rows but that needs connection of datasource with your views . You have to change your dataset first and then you have to call
[self.tableview reloadData];
another method is that, after changing dataset call
[self.tableview reloadRowsAtIndexPaths:indexPath withAnimation:animation];
2nd method requires indexPath i.e. you know that which cell you need to edit .
My problem was same . In my project there were 2 TextFields and 1 label in each cell . Now depending on the values of 2 textfields, I have to show their multiplication in UILabel. and this is my code.
-(void)textFieldDidChange:(id)sender{
UITextField *_sender = (UITextField *)sender;
int tag = _sender.tag;
int row = tag / 3;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
((UILabel *)[[self.tableView cellForRowAtIndexPath:indexPath] viewWithTag:row * 3 + 2]).text = #"hello";
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(#"here");
static NSString *CellIdentifier = #"procedureDetailsCell";
UITableViewCell *cell = (procedureCell *)[self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if ( cell == nil ) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
[((procedureCell *)cell).Quantity setTag:indexPath.row + 0];
[((procedureCell *)cell).Quantity addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
[((procedureCell *)cell).Cost setTag:indexPath.row + 1];
[((procedureCell *)cell).Cost addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
[((procedureCell *)cell).total setTag:indexPath.row + 2];
return cell;
}
Logic is simple I have used Custom TableViewCell which contains 2 textfields and 1 label. when one of the two textfield's value is changed we are calling "textFieldDidChange" method which finds indexpath and then find UITableViewcell and then updates its lastview's text value, that is our UILabel. we have to give unique tag to each of our views .
Hey guys i've been stuck on this problem for 2 days in a row, so i'm asking if there is anybody out there that can give me a hand.
I have a tableview that is made up of 4 sections.
section 1 ->made up of just 1 row where its cell contains a subview which is a uiimageview
section 2 ->made up of 2 normal rows(just 2 plain simple cells with text in them)
section 3 -> made up of 1 normal row
section 4 -> made up of 1 row, where its cell contains a subview which is a uitextview that can contain dynamic text, so the uitextview's height and consequently the cells height varies depending on how much text is in the uitextview.
Here's the code to create this structure:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//create a nsstring object that we can use as the reuse identifier
static NSString *CellIdentifier = #"Cell";
//check to see if we can reuse a cell from a row that has just rolled off the screen
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//if there re no cells that can be reused, create a new cell
if(cell==nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
switch (indexPath.section) {
case 0:
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell.contentView addSubview:_viewForImageHeader];
break;
case 1:
cell.selectionStyle = UITableViewCellSelectionStyleGray;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.numberOfLines = 0;
cell.textLabel.lineBreakMode = 0;
cell.textLabel.font = [UIFont fontWithName:#"AmericanTypewriter" size:16.0];
break;
case 2:
cell.selectionStyle = UITableViewCellSelectionStyleGray;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [UIFont fontWithName:#"AmericanTypewriter" size:16.0];
break;
default:
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell.contentView addSubview:_textViewForArticle];
break;
}
}
else{
NSLog(#"in else");
}
//here i fill in the 2 normal cells with text
return cell;
}
When the uitableview loads(in portrait mode) everything is perfect(image is in section 1, section 2 and 3 contain their correct text and in section 4 i have my dynamic text). But when i start to rotate the app, all the cells get mixed up. For example i find the contents of section3 in section 4 and vice versa.
I think this has to with the fact that i am not maybe reusing the cells correctly. Should i use tags, and if so, how can i implement the use of tags in the specific case?
Put switch case outside if condition - after the if and else
Yes, this is because of reusing cells. You have a few options here, but if you will never have more than the 5 cells in this tableView, by far the easiest and most optimum solution is to not reuse cells. In other words, instead of calling "dequeueReusableCellWithIdentifier", just allocate a new cell each time.
When you have many more cells, this would degrade performance, but if your cells are static and limited (as they appear to be), there is no real gain from trying to reuse cells.
If you DID have 4 "types" of cells and more rows (more dynamically allocated cells), the solution would be to subclass UITableViewCell for each of the 4 "types" of cells and then to call the correct one based on the section or whatever your criteria is.