Filling UITableView using NSUserdefaults - ios

I've been trying for a while now to get the tableview to work correctly. Every day I am making a bit of progress but I am not able to see any kind of feedback by testing the app so I am not sure if I am doing something correctly or not.
I've searched on google for a while, looked at apple docs, but I still don't understand how I can populate the UITableView properly.
First problem. Where do I get the cell? I need to make a custom cell to display the data. The solution seems to be using a nib, but how can I load it on another view controller?
Second problem. This cell should obviously have dynamic content. This content would be picked up from the user defaults (I am able to do that), but I have no idea how I could access the labels in the nibs to achieve this.
Third problem. I want to have different sections (each section has a different kind of cell to display), with different headers/footers. As always, here I find the documentation lacking. How can I achieve this?
Fourth, and most important problem. I have NO IDEA on how to load the cells. Apparently that is done by cellForRowAtIndexPath but I am unsure how it manages to do that.
Finally, here's the code (along with the stacktrace). Hope you can help me. Please, do not close this for duplicate. Firstly, it shouldn't be, secondly, it's not a duplicate if the duplicate question was asked 3+ years ago because of how much the libraries have been updated.
#import "profileViewController.h"
#import "../Objs/CCProfileObject.h"
#interface profileViewController ()
#property (weak, nonatomic) IBOutlet UITableView *tableView;
#property (strong, nonatomic) IBOutlet UITableViewCell *proxySampleCell;
#property (strong, nonatomic) IBOutlet UITableViewCell *ccSampleCell;
#end
#implementation profileViewController
#synthesize tableView;
- (void)viewDidLoad{
// Dark mode, you can skip all of this
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(changeColor:)
name:#"darkToggle" object:nil];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if([userDefaults boolForKey:#"darkMode"] == YES){
[self changeColor:nil];
}
}
- (void)viewDidAppear:(BOOL)animated{
// Display profiles
NSData *arrayData = [[NSUserDefaults standardUserDefaults] objectForKey:#"profileArray"];
NSArray *profiles = [NSKeyedUnarchiver unarchiveObjectWithData:arrayData];
for(NSObject *profile in profiles){
if([profile class] == [CCProfileObject class])
{
CCProfileObject *ccProfile = (CCProfileObject*) profile;
printf("%s\n", [ccProfile.getName UTF8String]);
// Retrieve info from the object
// Duplicate cell
// Change appropriate labels
// Add cell to a specific section
}
// Different types of cells will be added later
// Concept is the same, what changes is the specific section and cell to duplicate
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
if(indexPath.section == 1){
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"reusableShippingCell"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"reusableShippingCell"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
cell.textLabel.text = #"aldo";
return cell;
}
return nil;
}
// This should be the only part that actually works (I use the same class twice because proxy and shipping are not yet done)
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
// Display profiles
NSData *arrayData = [[NSUserDefaults standardUserDefaults] objectForKey:#"profileArray"];
NSArray *profiles = [NSKeyedUnarchiver unarchiveObjectWithData:arrayData];
NSMutableArray *sectionData = [[NSMutableArray alloc]init];
for(NSObject *profile in profiles){
// Credit
if([profile class] == [CCProfileObject class])
{
[sectionData insertObject:#1 atIndex:0];
}
// Shipping
else if([profile class] == [UIImage class])
{
[sectionData insertObject:#1 atIndex:1];
}
// Proxy
else if([profile class] == [UIImage class])
{
[sectionData insertObject:#1 atIndex:2];
}
}
int toReturn = 0;
for(int i = 0; i < [sectionData count]; i++){
toReturn += [[sectionData objectAtIndex:i] integerValue];
}
return toReturn;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
// Credit card section
if(section == 0){
return 1;
} else if (section == 1){
return 2;
} else {
return 3;
}
}
`Assertion failure in -[UITableView _configureCellForDisplay:forIndexPath:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3698.52.10/UITableView.m:9453
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView (<UITableView: 0x16106d000; frame = (0 0; 375 812); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1d0243bd0>; layer = <CALayer: 0x1d0030f60>; contentOffset: {0, -88}; contentSize: {375, 117}; adjustedContentInset: {88, 0, 83, 0}>) failed to obtain a cell from its dataSource (<profileViewController: 0x15ff14c70>)
First throw call stack:
(0x1837f6d8c 0x1829b05ec 0x1837f6bf8 0x1841e6fa0 0x18d4975b0 0x18d4941e8 0x18d493e00 0x18d492b1c 0x18d48e668 0x18d3cb770 0x18796d25c 0x1879713ec 0x1878ddaa0 0x1879055d0 0x18d7b56b4 0x18379f2bc 0x18379ea7c 0x18379c7b0 0x1836bcda8 0x18569f020 0x18d69d78c 0x1048a4f90 0x18314dfc0)
libc++abi.dylib: terminating with uncaught exception of type NSException
You are not required to solve all of my problems with a single answer. Even fixing a problem/explaining what I need to do is enough as that will allow me to at least move a bit forward.

First, you don't need a custom nib for UITableViewCell unless you have a really complex cell. Rather than that, you can directly design your cell layout using Prototype Cells in UITableView's Attributes inspector. Just give Prototype Cells a number & design appropriate cell's layout inside UITableView view. Don't forget to set identifier for each of them, that will help you load correct layout in cellForRowAtIndexPath.
For simple cell layout, you can provide each of elements of the cell a tag number. This will help you access them whenever you need them like this:
UILabel *label = [cell viewWithTag:100];
Every UITableView needs a DataSource, you will implement UITableViewDataSource to provide:
Number of sections for your table
Number of items for each section
Content for individual cell
You just did it in your provided source code, except one thing: cellForRowAtIndexPath requires a UITableViewCell to be returned, not nil. If you return nil, exception will be thrown.
So, a working cellForRowAtIndexPath might look like this:
#implementation ViewController {
NSArray *data;
}
- (void)viewDidLoad {
[super viewDidLoad];
// You load your data from NSUserDefault here
data = [self loadDataFromNSUserDefault];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *cellIdentifier = #"defaultCell";
if (indexPath.section == 0) {
cellIdentifier = #"customCell1";
} else if (indexPath.section == 1) {
cellIdentifier = #"customCell2";
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
// Access your view's children using their `tag`
UILabel *title = [cell viewWithTag:100];
UIImageView *icon = [cell viewWithTag:101];
// Set content
title.text = [[data objectAtIndex:indexPath.row] objectForKey:#"title"];
icon.image = [UIImage imageNamed:[[data objectAtIndex:indexPath.row] objectForKey:#"icon"]];
return cell;
}

Your core problem lies in this line - if (indexPath.section == 1) - the section numbering starts from 0. So you return nil for cells in your first section (numbered 0), and any section beyond the second one (numbered 1), which causes the crash. You should never return nil from cellForRow.
That being said - I still recommend you read through the links provided in the comments, you may still learn something valuable and/or interesting.

Related

UITableView after prepareForReuse - all the data in Cell disappear

I have a TableView with customCell, that has next properties:
#interface CustomTableViewCell : UITableViewCell
#property (weak, nonatomic) IBOutlet UIImageView *image;
#property (weak, nonatomic) IBOutlet UILabel *nameOfImage;
#property (weak, nonatomic) IBOutlet UIButton *start;
#property (weak, nonatomic) IBOutlet UIButton *stop;
#property (weak, nonatomic) IBOutlet UIProgressView *progressView;
#property (weak, nonatomic) IBOutlet UILabel *realProgressStatus;
In table view - when user press start button - an image is downloaded, as well as progressView with realProgressStatus reflects current situation. At this stage everything works perfect, but when scroll table view and return back to the cell - that was already fulfilled with data - all info disappeared(except the nameOfImage, I set it separately).
I implemented next method in my CustomTableViewCell class:
-(void)prepareForReuse
{
[super prepareForReuse];
self.progressView.progress = 0.1;
self.realProgressStatus.text = #"";
self.image.image = [UIImage imageNamed:#"placeholder"];
}
I implemented new property NSMutableSet self.tagsOfCells, where I save number of cells where images where already downloaded.
I tried to make changes in TableView method but effect is the same :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* PlaceholderCellIdentifier = #"Cell";
CustomTableViewCell *customCell = [tableView dequeueReusableCellWithIdentifier:PlaceholderCellIdentifier forIndexPath:indexPath];
customCell.delegate = self;
customCell.cellIndex = indexPath.row;
if (!customCell)
{
customCell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"cell"];
}
else
{
NSNumber *myNum = [NSNumber numberWithInteger:indexPath.row];
if (![self.tagsOfCells containsObject:myNum])
{
NSLog(#"Row: %ld",(long)indexPath.row);
UIImage *img = [UIImage imageNamed:#"placeholder"];
customCell.realProgressStatus.text = #"";
customCell.progressView.progress = 0.1;
customCell.image.image = img;
[customCell setNeedsLayout];
}
}
if ( indexPath.row % 2 == 0 )
customCell.backgroundColor = [UIColor grayColor];
else
customCell.backgroundColor = [UIColor darkGrayColor];
customCell.nameOfImage.text = self.names[indexPath.row];
return customCell;
}
EDIT:
I populate self.tagsOfCells in one of methods during downloading images, here is the method:
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
self.customCell.realProgressStatus.text = #"Downloaded";
UIImage *img = [UIImage imageWithData:self.imageData];
self.customCell.image.image = img;
self.customCell.tag = self.selectedCell;
[self.savedImages setObject:img forKey:self.customCell.nameOfImage.text];
NSNumber *myNum = [NSNumber numberWithInteger:self.selectedCell];
[self.tagsOfCells addObject:myNum];
}
Thanks in advance for any help or advice.
Content-related actions shouldn't occur in the prepareForReuse function.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewCell_Class/#//apple_ref/occ/instm/UITableViewCell/prepareForReuse
If a UITableViewCell object is reusable—that is, it has a reuse identifier—this method is invoked just before the object is returned from the UITableView method dequeueReusableCellWithIdentifier:. For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view's delegate in tableView:cellForRowAtIndexPath: should always reset all content when reusing a cell. If the cell object does not have an associated reuse identifier, this method is not called. If you override this method, you must be sure to invoke the superclass implementation.
Edited
Alfie is right, prepareForReuse isn't invoked after returning from cellForRowAtIndexPath
First thing I would try is edit the code
if (!customCell)
{
customCell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"cell"];
}
else
{
NSNumber *myNum = [NSNumber numberWithInteger:indexPath.row];
if (![self.tagsOfCells containsObject:myNum])
{
NSLog(#"Row: %ld",(long)indexPath.row);
UIImage *img = [UIImage imageNamed:#"placeholder"];
customCell.realProgressStatus.text = #"";
customCell.progressView.progress = 0.1;
customCell.image.image = img;
[customCell setNeedsLayout];
}
}
to the following
if (!customCell)
{
customCell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"cell"];
}
NSNumber *myNum = [NSNumber numberWithInteger:indexPath.row];
if (![self.tagsOfCells containsObject:myNum])
{
NSLog(#"Row: %ld",(long)indexPath.row);
UIImage *img = [UIImage imageNamed:#"placeholder"];
customCell.realProgressStatus.text = #"";
customCell.progressView.progress = 0.1;
customCell.image.image = img;
[customCell setNeedsLayout];
}
This really isn't an expensive operation and it's better to have this logic in a single place. Especially since you got rid of prepareForReuse, this should be changed.
The reason when you scroll away, and then come back to not find your data is cellForRowAtIndexPath is being called again, which results in the default data overriding the new data you just added.
You would want to have an underlying data structure and retrieve data from there.
What I would do:
For instances, you can keep a mutable list as such
NSMutableArray* tableData = [[NSMutableArray alloc] init];
add image data for each row to the array
[tableData add:imageData];
and then display it add cellForRowAtIndexPath
customCell.image.image = [tableData objectAtIndex:[indexPath row]];
This would be the normal practice for populating a UITableView with data. This way, even if you scroll away tableData would remain the same, resulting in consistent data.
Note
I would also call new data asynchronously using NSURLSession from within the cell if I had to implement this. Just a suggestion=)
I don't think this has anything to do with your use of prepareForReuse. A couple things look fishy.
You're setting the delegate and cellIndex properties on your cell before you know whether it's nil or not. If it's nil then these properties will never be set.
I also don't think you need the else clause in there. The logic that it wraps doesn't have anything to do with whether a cell could be dequeued. it has to do with configuring a non-nil cell.
A missing piece from your question is where do you populate the tagsOfCells object? That code is important. If you're not setting the tag in cellForRowAtIndexPath it seems like your data will get out of sync and this will cause your cell to be misconfigured.
Try the following code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* PlaceholderCellIdentifier = #"Cell";
CustomTableViewCell *customCell = [tableView dequeueReusableCellWithIdentifier:PlaceholderCellIdentifier forIndexPath:indexPath];
if (!customCell)
{
customCell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"cell"];
}
NSNumber *myNum = [NSNumber numberWithInteger:indexPath.row];
if (![self.tagsOfCells containsObject:myNum])
{
NSLog(#"Row: %ld",(long)indexPath.row);
UIImage *img = [UIImage imageNamed:#"placeholder"];
customCell.realProgressStatus.text = #"";
customCell.progressView.progress = 0.1;
customCell.image.image = img;
[customCell setNeedsLayout];
}
customCell.delegate = self;
customCell.cellIndex = indexPath.row;
customCell.nameOfImage.text = self.names[indexPath.row];
if (indexPath.row % 2 == 0)
{
customCell.backgroundColor = [UIColor grayColor];
}
else
{
customCell.backgroundColor = [UIColor darkGrayColor];
}
return customCell;
}
Edit
Regarding info in another answer here. prepareForReuse is not being called after you configure your cell. It's being called when you invoke dequeueReusableCell and hence before you configure your cell. I don't believe prepareForReuse is the problem.
It looks like you are not using interface builder's prototype cells so you are using the wrong dequeue reusable cell method. The correct one in your situation does not have an index path param e.g.
CustomTableViewCell *customCell = [tableView dequeueReusableCellWithIdentifier:PlaceholderCellIdentifier];
And as the others said you need to move your 2 lines of initialization after you alloc init your custom cell because it's nil before then. Eg you need to dequeue the cell and if nil create one. Then set your params on the cell now you have a valid instance.

Objective-C unrecongnized selector sent to instance

I am trying to set the label that is within a table view cell. I created an array of all the timers that I need in each cell. When I go to construct the cell and hook up the timer label it gives me the unrecognized selector sent to instance error. I have tried everything and can not figure out why I keep getting this. The updateTimerLabel method just returns the timerLabel value so I do not see what the issue is. Can someone help me find out how to fix this?
the error is on the timerLabel
the timersArray holds an array of all created timers.
Here is the cellForRowAtIndexPath method:
-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TimerViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"TimerViewCell"];
[[NSBundle mainBundle] loadNibNamed:#"TimerViewCell" owner:self options:nil];
tempCell = [timersArray objectAtIndex:indexPath.row];
cell = tempCell;
tempCell = nil;
cell.timerLabel.text = [cell updateTimerLabel];
return cell;
}
here is the TimerViewCell.h
#import <UIKit/UIKit.h>
#import "Timer.h"
#interface TimerViewCell : UITableViewCell
{
IBOutlet UILabel* timerLabel;
Timer* _timer;
}
#property (strong, nonatomic) IBOutlet UILabel* timerLabel;
- (id)initCellWithTimer:(Timer*)timer;
- (IBAction)startTimerButton;
- (NSString*)updateTimerLabel;
#end
Your need to allocate cell as follows
-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TimerViewCell* tempCell = [tableView dequeueReusableCellWithIdentifier:#"TimerViewCell"];
if(tempCell == nil){
NSArray *objects = [[NSBundle mainBundle] loadNibNamed:#"TimerViewCell" owner:self options:nil];
tempCell = [objects firstObject];
}
tempCell.timerLabel.text = [cell updateTimerLabel];
return tempCell;
}
since you are loading cell from xib. You check your outlet connection of your timerLabel.
and also check is your cell pointing to TimerViewCell? To check click on cell and check is class like image
Here's what's happening in the following three lines of your code:
tempCell = [timersArray objectAtIndex:indexPath.row];
cell = tempCell;
tempCell = nil;
You've assigned an element from timersArray to your class variable tempCell
Next you've pointed your TimerViewCell instance cell to this element.
Finally you remove the pointer that tempCell was holding by assigning it to nil, and since cell is pointed at tempCell, then you are also pointing cell at nil
Then in the next line, you attempt to call updateTimerLabel on nil
cell.timerLabel.text = [cell updateTimerLabel];
I think you'll have more luck with something like this:
- (void)viewDidLoad
{
[self.tableView registerNib:[UINib nibWithNibName:#"TimerViewCell" bundle:nil] forCellReuseIdentifier:#"TimerViewCell"];
}
-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TimerViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"TimerViewCell"];
cell.timerLabel.text = [cell updateTimerLabel];
return cell;
}
Well unlike java in ObjC pointers (direct memory locatioin reference) existing and anything you declare with *_instance style will be a pointer.
So when we are assigning one pointer to another they just refer to same location and changing pointer reference to a difreent location (null here) will aparently clean other as well.
tempCell = [timersArray objectAtIndex:indexPath.row];
cell = tempCell;
tempCell = nil;
after these three lines even cell will be pointing to a nil. and hence returning nil to cell which is causing crash.
So removing the line tempCell = nil; should solve the issue.

Table Cell not always showing Hidden Content after Unhiding

I have a table with a custom cell, in the cells are two UIViews the top one is always displayed and the bottom one is hidden.
When the cellCellForRowAtIndexPath is called the cells are all reduced in height to just show the top UIView.
When the user clicks the cell then that cell is expanded and then I unhide the second UIView. The problem is that sometimes the second UIView does not appear on first click on the cell.
Clicking the cell again makes it all disappear and then another click and it always appears perfectly. Scroll down and then pick another and first click it may not appear or will be in wrong place, two more licks and it is perfect.
I think it is a ReusableCell issue but I have no idea how to get around it.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
cellID = #"Cell";
customCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
// Test to see if the Cell has already been built
if (cell == nil)
{
cell = [[customCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}
// Check if the selected Cell is being drawn and unhide the pull down view and adjust y origin
if(indexPath.row == selectedCell)
{
// Unhide the view
cell.pullDownView.hidden = NO;
CGRect tempFrame = cell.pullDownView.frame;
tempFrame.origin.y = [[secondViewY objectAtIndex:indexPath.row] floatValue];
cell.pullDownView.frame = tempFrame;
// Image in second view
cell.mainImage2.image = [theImages objectAtIndex:indexPath.row];
}
else
{
cell.pullDownView.hidden = YES;
}
// Image in first view
cell.mainImage.image = [theImages objectAtIndex:indexPath.row];
return cell;
}
So clicking a cell calls the didSelectRow which looks like this
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(selectedCell == indexPath.row)
{
selectedCell = -1;
[tableView deselectRowAtIndexPath:indexPath animated:NO];
}
else
{
selectedCell = indexPath.row;
}
// [tableView reloadData];
[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationMiddle];
[tableView endUpdates];
}
I have tried both reloadData and BeginUpdates as you can see but this still does not work. Is this because I am hiding/unhiding should I do Alpha to 0 and then 1 when selected? Is this a reusable cell thing?
Ohhh for completeness. You may notice I have a table where all the heights are different which is why I change the Y of the second UIView.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
float returnHeightValue;
if(indexPath.row == selectedCell)
{
returnHeightValue = [[selectedCellHeights objectAtIndex:indexPath.row] floatValue];
}
else
{
returnHeightValue = [[normalCellHeights objectAtIndex:indexPath.row] floatValue];
}
return returnHeightValue;
}
Okay I have changed how I address the items. I have removed the UIView (which they were all embedded in) and instead I now have in my .h of my custom cell.
#property (nonatomic, strong) IBOutletCollection(UILabel) NSArray *pullDownItems;
#property (weak, nonatomic) IBOutlet UIImageView *mainImage;
Rather than
#property (weak, nonatomic) IBOutlet UIView *pullDownView;
Okay so now I have to unhide and move two items rather than one but that is way better than ding them individually would have been a pain (there is around 10 UILabels). I have enumerate the array like so;
for(UILabel *obj in cell.pullDownItems)
{
obj.hidden = NO;
tempFrame = obj.frame;
tempFrame.origin.y = tempFrame.origin.y - tempYOffset;
obj.frame = tempFrame;
}
Any new items I just just attach.
Okay changed it all around as hiding and unhiding content on a cell that changed did not work so well (got artifacts left in the table and sometimes things would not show up).
I am now creating two custom cells one has all the additional objects when the cell is selected the other does not.
So far seems to be much better way of doing it.

Getting SIGABRT error with simple storyboard app

I have a problem that I've searched the internet for a solution for with no avail. I'm following along in chapter 10 of Apress' Beginning iOS 5 development which is on storyboards.
I'm thinking the book is missing a simple piece of code and since I'm going through this for the first time, I don't know how to resolve it.
I've restarted the project twice, but keep getting this error in the debugger:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
#import "BIDTaskListController.h"
#interface BIDTaskListController ()
#property (strong, nonatomic) NSArray *tasks;
#end
#implementation BIDTaskListController
#synthesize tasks;
- (void)viewDidLoad {
[super viewDidLoad];
self.tasks = [NSArray arrayWithObjects:
#"Walk the dog",
#"URGENT:Buy milk",
#"Clean hidden lair",
#"Invent miniature dolphins",
#"Find new henchmen",
#"Get revenge on do-gooder heroes",
#"URGENT: Fold laundry",
#"Hold entire world hostage",
#"Manicure",
nil];
}
- (void)viewDidUnload {
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
self.tasks = nil;
}
//
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return [tasks count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = nil;
NSString *task = [self.tasks objectAtIndex:indexPath.row];
NSRange urgentRange = [task rangeOfString:#"URGENT"];
if (urgentRange.location == NSNotFound) {
identifier = #"plainCell";
} else {
identifier = #"attentionCell";
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
UILabel *cellLabel = (UILabel *)[cell viewWithTag:1];
cellLabel.text = task;
return cell;
}
#end
lack of code:
...
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
UILabel *label = [[UILabel alloc] initWithFrame:cell.bounds];
label.tag = 1;
[cell addSubview:label];
}
UILabel *cellLabel = (UILabel *)[cell viewWithTag:1];
cellLabel.text = task;
....
Actually, I think that your original problem isn't anything missing from the code, it's missing from your storyboard configuration! IIRC, the book describes creating two cell prototypes in the storyboard: One with red text and its identifier set to "attentionCell", and one with black text and its identifier set to "plainCell". If those two cells exist within the tableview in the storyboard, with the right identifiers set, then the original piece of code you posted should work just fine.
There is no missing code. I followed the instructions in Chapter 10 exactly, and it compiled and ran fine.
The top of page 363 explains that a table view loaded from a storyboard can create cells on demand, and there is no need to check for a nil return.

How to purge a cached UITableViewCell

Does anyone have suggestions on how to purge a cached UITableViewCell?
I'd like to cache these cells with reuseIdentifier. However, there are times when I need to delete or modify some of the table rows. I expect to call reloadData after the row changes.
Right now, dequeueReusableCellWithIdentifier always returns the cached(obsolete) entry from before. How do I indicate that the cache is stale and needs to be purged?
I'm not sure why you're trying to purge cells in the first place. Every time you dequeue a cell, you need to re-set any data that needs to be displayed. The caching just prevents you from having to set up any non-changing properties every time. But the actual data that's being displayed must be set, even if the cell was cached before.
Note that your reuse identifier is supposed to be the same for all the cells of the same type. If you're doing something silly like calculating the identifier based on the row in question, then you're doing it wrong.
Your code should look something like
- (UITableViewCell *)tableView:(UITableView *)view cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = #"CellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
// no cached cell, create a new one here
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier] autorelease];
// set any properties that all such cells should share, such as accessory, or text color
}
// set data for this particular cell
cell.textLabel.text = #"foo";
return cell;
}
In that example, note how I always set the data for the cell every single time, and all cells share the same identifier. If you follow this pattern, you should have no reason at all to try and "purge" your cells, as any old data will be overwritten anyway. If you have multiple types of cells, you may want to use multiple identifiers in that case, but it's still 1 identifier per cell type.
I don't know how to purge the cache, however, I use a workaround how to handle the situation, when the rows need to be changed.
First of all, I don't use static cell identifier. I ask the data object to generate it (pay attention to [myObject signature], it provides a unique string describing all needed properties):
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyObject *myObject = _dataSource[indexPath.row];
NSString *cellId = [myObject signature];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
[myObject configureCell:cell];
}
[myObject updateCellWithData:cell];
return cell;
}
[myObject signature] provides different signatures for different list of properties. So, if my object is changed, I just call [self.tableView reloadData], myObject will provide a new signature, and the table load a new cell for it.
[myObject configureCell:cell] places all needed subviews to the cell.
[myObject updateCellWithData:cell] updates the subviews of the cell with current data.
while ([tableView dequeueReusableCellWithIdentifier:#"reuseid"]) {}
I have found a really good method.
In .h
//add this
int reloadCells;
In .m
- (void)dumpCache {
reloadCells = 0;
[tableView reloadData];
}
-(void)tableView:(UITableView *)tableView cellForRowAtUndexPath:(NSIndexPath *)indexPath {
static NSString *CellID = #"Cell"
UITableViewCell *cell = [tableView dequeCellWithReuseIdentifier:CellID];
if (cell == nil || reloadCells < 12) {
cell = [[UITableViewCell alloc] initWithFormat:UITableViewCellStyleDefault reuseIdentifier:CellID];
reloadCells ++;
}
cell.textLabel.text = #"My Cell";
return cell;
}
I had to something similar today. I have a gallery of images, and when the user deletes them all, the gallery needs to get rid of the last queued cell and replace it with a "gallery empty" image. When the delete routine completes, it sets a 'purge' flag to YES, then does a [tableView reloadData]. The code in cellForRowAtIndexPath looks like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
NSString *cellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: cellIdentifier];
if (purge) {
cell = nil;
purge = NO;
}
if (cell == nil) {
//*** Do usual cell stuff
}
return cell
}
I am coding ObjC (manual reference counting) and found some strange behavior that prevents UITableView and UITableViewCells from getting released, although my memory management is correct. UITableView and UITableViewCell seem to retain each other. And yes, I found a way to force the UITableView and its cells to release each other (remove cached cells).
To do this, basically you ask a table view for a reusable cell. The table view will remove it from its reusable cells cache. After all you tell that cell to remove from its superview. Done.
Now as a stand-alone class... First, you need an array of all cell identifiers you used. Next, you implement a dummy data source and let that data source clear the UITableView by reloading itself with the "ClearAllCachedCells" Data source:
#import <UIKit/UIKit.h>
#interface ClearAllCachedUITableViewCellsDataSource : NSObject <UITableViewDataSource, UITableViewDelegate>
+(void)clearTableView:(UITableView*)tv
reuseIdentifiers:(NSArray<NSString*>*)cellIdentifiers
delegateAfterFinish:(id<UITableViewDelegate>)dg
dataSourceAfterFinish:(id<UITableViewDataSource>)ds;
#end
And the magic happens in the .m file:
#import "ClearAllCachedUITableViewCellsDataSource.h"
#interface ClearAllCachedUITableViewCellsDataSource () {
BOOL clearing;
}
#property (nonatomic, readonly) UITableView *tv;
#property (nonatomic, readonly) NSArray<NSString*> *identifiers;
#property (nonatomic, readonly) id ds;
#property (nonatomic, readonly) id dg;
#end
#implementation ClearAllCachedUITableViewCellsDataSource
-(void)dealloc {
NSLog(#"ClearAllCachedUITableViewCellsDataSource (%i) finished", (int)_tv);
[_tv release];
[_ds release];
[_dg release];
[_identifiers release];
[super dealloc];
}
-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {
[self performSelectorOnMainThread:#selector(clear) withObject:nil waitUntilDone:NO];
NSLog(#"TV (%i): reloading with zero cells", (int)_tv);
return 0;
}
-(void)clear {
if (!clearing) {
clearing = YES;
for (NSString *ident in self.identifiers) {
UITableViewCell *cell = [_tv dequeueReusableCellWithIdentifier:ident];
while (cell) {
NSLog(#"TV (%i): removing cached cell %#/%i", (int)_tv, ident, (int)cell);
[cell removeFromSuperview];
cell = [_tv dequeueReusableCellWithIdentifier:ident];
}
}
self.tv.delegate = self.tv.delegate == self ? self.dg : self.tv.delegate;
self.tv.dataSource = self.tv.dataSource == self ? self.ds : self.tv.dataSource;
[self release];
}
}
+(void)clearTableView:(UITableView*)tv
reuseIdentifiers:(NSArray<NSString*>*)cellIdentifiers
delegateAfterFinish:(id<UITableViewDelegate>)dg
dataSourceAfterFinish:(id<UITableViewDataSource>)ds {
if (tv && cellIdentifiers) {
NSLog(#"TV (%i): adding request to clear table view", (int)tv);
ClearAllCachedUITableViewCellsDataSource *cds = [ClearAllCachedUITableViewCellsDataSource new];
cds->_identifiers = [cellIdentifiers retain];
cds->_dg = [dg retain];
cds->_ds = [ds retain];
cds->_tv = [tv retain];
cds->clearing = NO;
tv.dataSource = cds;
tv.delegate = cds;
[tv performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
}
}
#end
The old data from previous usage of the cell should be cleared with the message prepareForReuse. When a cell is dequeued this message is sent to the cell before it is returned from dequeueReusableCellWithIdentifier:.
No way, I think...
I used a completely different approach. Instead of relying on UITableView cache, I build my own. In this way I have perfect control on when and what to purge.
Something like this...
given:
NSMutableDictionary *_reusableCells;
_reusableCells = [NSMutableDictionary dictionary];
I can do:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellIdentifier = #"whatever-you-need";
UITableViewCell *cell = [_reusableCells objectForKey:cellIdentifier];
if (cell == nil)
{
// create a new cell WITHOUT reuse-identifier !!
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
// store it in my own cache
[_reusableCells setObject:cell forKey:cellIdentifier];
/* ...configure the cell... */
}
return cell;
}
and:
-(void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
[_reusableCells removeAllObjects];
}
Hope this may help.

Resources