I am producing an iPhone app for which part of the interface is exactly like the 'Most Popular' section of the iPhone YouTube app.
This 'popular' section is accessed from a Tab Bar at the bottom and the navigation bar at the top contains a UISegmentedControl to select 'Today, This Week, Month etc..'
Because most of the app consists of UITableViews with cells containing very similarly structured content, I have created a common MyAppTableViewController which inherits UITableViewController. My 'popular' section thus consists of a PopularTableViewController which inherits MyAppTableViewController. The actual UITableView resides within MyAppTableViewController.
PopularTableViewController has the method:
- (void) segmentChangeTimeframe:(id)sender {
UISegmentedControl *segCtl = sender;
if( [segCtl selectedSegmentIndex] == 0 )
{
// Call [self parse-xml-method-which-resides-in-MyAppTableViewController]
}
//... ... ...
}
The MyAppTableViewController makes use of NSXMLParser and thus has the code:
- (void)parserDidEndDocument:(NSXMLParser *)parser {
[self.myTableView reloadData];
}
(There are other methods which updates the data structure from which the table view gets it's data)
I have put console output code into the xml parsing methods, and when run, selecting the different segments causes the correct xml files to be parsed fine and the data structure seems to contain the correct values.
The problem is that the contents of the table cells wont change! grr! UNLESS!... A cell is scrolled out of view, and then back into view... THEN its changed!
I have done lots of searching about for this problem and one suggestion for a similar problem was to place the [self.myTableView reloadData] into its own method e.g. myReloadDataMethod and then use:
[self performSelectorOnMainThread:#selector(myReloadDataMethod) withObject:nil waitUntilDone:NO];
I tried placing the above code into the parserDidEndDocument method and it made absolutely no difference! I'm absolutely stumped and am wondering if anybody has any idea what's going on here.
Update:
The code to populate the cells is done with:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:MyIdentifier] autorelease];
}
// Set up the cell
int itemIndex = [indexPath indexAtPosition: [indexPath length] - 1];
NSString *artistName = [[myItemList objectAtIndex: itemIndex] objectForKey: #"itemA"];
NSString *mixName = [[myItemList objectAtIndex: itemIndex] objectForKey: #"itemB"];
cell.textLabel.text = itemA;
cell.detailTextLabel.text = itemB;
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
return cell;
}
The above code is in MyAppTableViewController which is also where myItemList resides.
Your -performSelectorOnMainThread: code is for when you make changes to the model classes on a background thread. UI events (including -reloadData) need to occur on the main thread. If you're not using a background thread, then this is unnecessary. If you are, something like it is mandatory.
If you are changing the value of a specific cell, the way you achieve that is to change the cell itself. On iPhone, cells are full views (unlike on Mac), so if you want to change their data, you just change their data and call -setNeedsDisplay. You can get the cell (view) for a given location using -cellForRowAtIndexPath:. You can determine if a given cell is onscreen by using -indexPathsForVisibleRows or -visibleCells.
It is very rare to need to call -reloadData. You should only do that if you are throwing away everything and loading completely different data. Instead, you should use the insertion/deletion routines to add/remove rows, and you should just update the views of existing rows when their data change.
I had this same problem, and it was because I had a [tableView beginUpdates] call without an endUpdates call after.
Have you tried [tableView setNeedsDisplay:YES]?
After calling -reloadData, do you recieve callback to tableView:numberOfRowsInSection: ?
I'm almost sure, that self.myTableView is nil here:
- (void)parserDidEndDocument:(NSXMLParser *)parser {
[self.myTableView reloadData];
}
Related
I have a prototype table in my app witch I populate with a customTableViewCell class with a UITextField inside.
In my navigation bar I got a save button.
The question is, how to access this dynamic created cell's to get the UITextField content?
This is my code, you can see that I tried to use NSMutableArray
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"customTableCell";
customTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[self.pfCells addObject:cell];
if(cell == nil)
{
cell = [[customTableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
// Configuration
cell.lblName.text = [self.pfFields objectAtIndex: [indexPath row]];
cell.txtType = [self.pfTypes objectAtIndex: [indexPath row]];
if ([[self.pfTypes objectAtIndex:[indexPath row]] isEqualToString: #"n"]) {
[cell.txtField setKeyboardType:UIKeyboardTypeNumberPad];
} else if ([[self.pfTypes objectAtIndex:[indexPath row]] isEqualToString: #"m"]) {
[cell.txtField setKeyboardType:UIKeyboardTypeEmailAddress];
}
return cell;
}
Here's another way to save content from a UITextField contained in a UITableViewCell:
Inside tableView:cellForRowAtIndexPath: set the delegate and a tag for txtField
Implement textFieldDidEndEditing: check for a UITextField tag value an save data in a private variable
Reload UITableView
The biggest advantage of this implementation if the fact that you doesn't need to iterate over whole tableview everytime you change a textfield value.
Quick answer:
#pragma mark - UITextFieldDelegate
- (void)textFieldDidEndEditing:(UITextField *)textField
{
// grab the row we are working on
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
// remove the old key/value pair if it exists and add the new one
[self.modelDictionary removeObjectForKey:indexPath];
[self.modelDictionary setObject:textField.text forKey:indexPath];
}
Be sure to add cell.txtField.delegate = self when configuring your cell. Then in your save button, you'd iterate through the dictionary and save the values -- or just save the dictionary itself.
Also, if you are targeting iOS6 or later, use dequeueReusableCellWithIdentifier:forIndexPath: as this method guarantees a cell is returned and resized properly, so you don't have to check for nil and manually init your cell.
Longer answer:
You generally never want to store your model in your view as you are doing. Aside from it breaking the MVC design patterns, it also causes issues with UITableViews. Specifically, a UITableViewCell will be recycled when it scrolls off the screen. So any values you have in those fields are lost. While you can get away with doing this if you only have visible rows that never scroll off the screen, I would encourage you to avoid this approach altogether.
Instead, you should store the values entered into the textboxes in your model object. The easiest way to do this is to use UITextFieldDelegate's textFieldDidEndEditing: to grab the values after the user enters them, then add these values to your model. You model could be something as simple as an NSDictionary using the indexPath as the key.
So I get that its typically frowned upon to modify a cell outside of the cellForRowAtIndexPath but here is what I have:
I have a static table that is used as an index of questions (1-33). Each row has a question on it and a detail disclosure indicator. All of this is manually entered in on the stoyboard.
I have a file that lists each question and some properties such as if the question has been answered.
When this screen loads (viewDidAppear) I want to check if each of these questions have been loaded and if so switch the detail indicator to a checkmark.
Now this works, for the first 5 cells. If I go to a question and come back, then even more cells are checked (even if the questions have not been answered). Is this undefined behavior because I am accessing it outside of cellForRowAtIndexPath?
Here is the code I'm using to access and change the cell:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (question1Answered)
{
UITableViewCell *cell1 = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:3]];
[cell1 setAccessoryType:UITableViewCellAccessoryCheckmark];
}
}
Again, it does work for the first 5 elements, then the rest will not change no matter what I do. Then if I go to a question and return it shows more with it selected. Strange behavior...
EDIT: I just noticed that the above code works but it only updates the cells that are currently on the screen. So if I scroll down, leave and come back all the visible cells will have the check mark. Is there a way to force a refresh of all the cells, even if they aren't visible?
Thanks for any and all help...
-David
This is similar to another question I answered few days ago. See stackoverflow.com/a/11770387/1479411
Use delegate method. Put any code that modifies the cell content and [self.tableView reloadData] in the delegate method after returning from the other view controller.
You should not update cell from viewDidAppear.
Instead you should reload data from viewDidApeear.
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (question1Answered)
{
//This will call your tableview's delegate for visible cells
[self.tableView reloadData];
}
}
And inside cellForRowAtIndexPath, you should take a decision to assign accessory type.
U should first update your model then update your UI according to the model state.
For example if your model is an array of Question object, and each question has some hasBeenAnswered boolean.
Then the only thing u should do in viewDidAppear is to call [self.tableView reloadData], this will update your table view because cellForRowAtIndexPath will be called and set the cells according to your model state.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// configure the cell according to your model state
Question *question = [self.questions objectAtIndex:indexPath.row];
// check if this question has been answered
if (question.hasBeenAnswered) {
// if yes - set a checkmark
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
}
else {
// if not - set to none (or whatever u want)
[cell setAccessoryType:UITableViewCellAccessoryNone];
}
return cell;
}
Basically I'm making a list view that you can add things to the top of. The best way I can think of doing this is to store the UITableViewCells themselves in a NSMutableArray — Because I can simply pull them from the array them with all their data inside the object, and this list view will never be over 10 cells long.
Also note that I'm using Storyboards, hence the initWithCoder use.
The following code is what I'm trying, and it doesn't work:
// This is where my NSMutableArray is initialized:
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder]) {
if (!_CellsArray) {
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"TestCell"];
_CellsArray = [NSMutableArray arrayWithObject:cell];
}
}
return self;
}
//UITableView Delegate & DataSource Methods
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"TestCell"];
[_CellsArray insertObject:cell atIndex:0];
return [_CellsArray objectAtIndex:indexPath.row];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 10;
}
I realize I may be approaching this in the wrong way, that's why I'm here though :)
Thank you.
edit: fixed a type in the code (TimerCell -> UITableViewCell)
Let's look at the order things get called in and what happens.
Your view controller is unarchived, so your initWithCoder: method is called. This method creates a mutable array and puts one instance of TimerCell into it. Said instance is not further configured (unless you've overridden initWithStyle:reuseIdentifier: to do some configuration).
Your data source method tableView:numberOfRowsInSection: is called, and it tells the table view there are ten rows.
Thus, your tableView:cellForRowAtIndexPath: is called ten times. Each time, it creates a new instance of UITableViewCell and inserts it into your mutable array. (After ten calls, your mutable array contains one TimerCell at index 10 and ten UITableViewCells at indices 0-9.) It does nothing to configure the cell's contents or appearance, then it returns the cell at the specified row index. On the first call, you're asked for row 0, so the cell you just created and inserted at index 0 is returned. On the second call, you're asked for row 1, so the cell at index 1 in your array is returned -- since you just inserted a new cell at index 0, the cell you created on the last call has shifted to index 1, and you return it again. This continues with each call: you return the same unconfigured UITableViewCell ten times.
It looks like you're trying to out-think UIKit. This is almost never a good thing. (It's been said that premature optimization is the root of all evil.)
UITableView already has a mechanism for cell reuse; it's best to just keep track of your own cell content and let that mechanism do its thing. I took so long to type this that other answers have been written describing how to do that. Look to them, or to Apple's documentation or any third-party UITableView tutorial.
Why don't you just store the cell information in an array. Then in the -cellForRowAtIndexPath: method, just extract the data needed to change each cell.
Here is a simple example:
//Lets say you have an init like this that inits some cell information
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder]) {
cellArray = [NSArray alloc] initWithObjects:#"firstCell",#"secondCell",#"thirdCell",nil];
}
return self;
}
//then for each cell, just extract the information using the indexPath and change the cell that way
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
cell.textLabel.text = [cellArray objectAtIndex:indexPath.row];
return cell;
}
Table views don't store things. Rather, they just ask for the data they want to display, and you typically get that data from elsewhere (like an NSArray, or an NSFetchedResultsController). Just store the things you want into some data container, and let the table display them for you.
// Probably your data model is actually a member of your class, but for purposes of demonstration...
static NSArray* _myArray = [[NSArray alloc] initWithObjects:#"Bob", #"Sally", #"Joe", nil];
- (NSInteger) tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
return [_myArray count];
}
- (UITableViewCell*) tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
static NSString* CellIdentifier = #"TestCell";
// Make a cell.
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if( cell == nil ) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// Setup the cell with the right content.
NSString* aString = [_myArray objectAtIndex:[indexPath row]];
cell.textLabel = aString;
return cell;
}
Now if you want more stuff in the list, add it to your array, and you're done.
Edit: On another note, initWithCoder: isn't generally the best place to do initialization for a view controller. Reason being, at the point that it's called, there's a good chance that stuff isn't loaded yet (IBOutlets, for example). I tend to prefer viewDidLoad (don't forget to cleanup in viewDidUnload in that case), or awakeFromNib.
I have a table view with custom cells, each of which have images and some text which must be parsed from a webpage. I have and operation queue which gets the data from the page and calls the method (void)addLoadedImageAndExcerpt:(NSString *)imgExc in the tableviewcontroller after each page's data is loaded and stores the data in 2 arrays. I need each cell to refresh once the image and text that associated with it are loaded into these 2 arrays (named "articleExcerpts" and "imageDataObjects").
the method is as follows:
- (void)addLoadedImageAndExcerpt:(NSString *)imgExc {
NSArray *imgAndExcerpt = [imgExc componentsSeparatedByString:#"{|}"];
[articleExcerpts addObject:[imgAndExcerpt objectAtIndex:1]];
NSData * imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString: [imgAndExcerpt objectAtIndex:0]]];
[imageDataObjects addObject:imageData];
//count how many rows have been loaded so far.
loadedCount ++;
[self.table reloadData];//table is a UITableView
[imageData release];
}
the problem is, I can't get the cells to change while they are on screen. Once I scroll, they show the proper data, while they are on screen, I can't get them to change. I tried the methods outlined here and here, but they don't work. I tried calling tableView:cellForRowAtIndexPath: for the relevant row and modifying the variables, but that didn't solve anything because that method seems to create a new cell every time is is called, and doesn't get the existing ones (I'll post the code for that method further down).
Using [self.table reloadData] as I have it now doesn't seem do anything either, which really confuses me...
my tableView:cellForRowAtIndexPath: method (I bet the problem is here. I'm not convinced I creating my custom cells properly)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CustomizedCell";
CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
for (id currentObject in topLevelObjects){
if ([currentObject isKindOfClass:[UITableViewCell class]]){
cell = (CustomCell *) currentObject;
break;
}
}
}
// Configure the cell...
//title
cell.titleString = [titles objectAtIndex:indexPath.row];
//date
cell.dateString = [dates objectAtIndex:indexPath.row];
//Photo. check if imageDataObjects array is complete up to the current row yet
if (loadedCount > indexPath.row) {
if ([imageDataObjects objectAtIndex:indexPath.row] != #"NA") {
cell.imageData = [imageDataObjects objectAtIndex:indexPath.row];
} else {
cell.imageData = NULL;
}
}
//Excerpt. check if loadedCount array is complete up to the current row yet
if (loadedCount > indexPath.row) {
cell.exerptString = [articleExcerpts objectAtIndex:indexPath.row];
}
return cell;
}
what am I missing?
I have had a similar problem before, and was able to get it working by including the lines
[table beginUpdates];
[table endUpdates];
at the end of the method where your data is received (so call them once you have the data to populate the cells).
Hope this works for you too!
Hmm, I think you're only supposed to interact with UI components in the main thread. NSOperationQueue stuff runs in another thread. Instead of calling
[self.table reloadData]
try
[self.table performSelectorOnMainThread:#selector(reloadData:) withObject:nil waitUntilFinished:NO]
As far as I understand, the image is being loaded, and then added to an array of images (imageDataObjects), and the row never updates.
First things first, are you sure that the method addLoadedImageAndExcrept is adding the images in order? Remember that NSArray objects are nil-terminated, and therefore, if you're adding an image for a row further, it won't appear if a previous image is nil. What happens if an image comes nil? The array will end abruptly. Use the "count" method on the array to check if this happens, add dummy objects, or swtich to a dictionary. This may not solve your current issue, but it's something to consider. (*)
Aside from that, if images are being loaded correctly, the only reason for your code to not work (in what I understand from the code), is that the table IBOutlet you added, is not connected.
*EDIT: I noticed that you're checking for #"NA" on the row (although I don't see where it's being set), so you probably already considered that
In my iPhone app, I am using a single tableview to display different sets of data based on the button clicked.
Now as I am using the same tableView I need to blank out the tableView contents everytime a new button is selected.
And this is quite normal requirement rite? As such it is inefficient to take 7 tables to show 7 different data sets.
Problem:
I have seen that table clears out but when we display some other data in the table then the previous data appears in background as in Screenshot AFTER.
I have tried setting the array as nil and reloading the tableView but then it doesnt seem to work.
What can be a fix for this issue?
I have checked the code and it seems proper to me.
You can refer to the Screen shot to get a better idea of what actually is happening.
BEFORE ( i.e. the first time Event is clicked)
AFTER (i.e. once the Event category button is clicked after some other category button)
You can clearly see a different image in background where as it should be same as image in above screenshot. This is not a button, I am adding a UIImageView to tableViewCell.
NSArray is not mutable, that is, you cannot modify it.
Instead of using NSArray use NSMutableArray and use
[mutArr removeAllObjects];
and then reload the tableView. It worked for me.
- (IBAction)yourAction {
tableView.delegate = nil;
tableView.dataSource = nil;
[tableView reloadData];
}
it will empty your table view... ur need is not clear
In button action method, call the tableView reload and assign null to object from which you are initializing the cells previously,
[tableView reloadData];
You can return zero for numberOfRowsInSection, and add BOOL variable for isEmpty.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if(isEmpty){
return 0;
}else{
// your current logic
}
}
//action when button clicked
-(IBAction)myAction{
isEmpty = TRUE;
[self reloadData];
}
I don't think it will work if your array is nil. Try initializing it as a empty array with:
myArr = [NSArray array];
And then reload the tableView data. Otherwise I think we need to see your code
EDIT
It is still a bit unclear(still no code in your question), but I think your problem is really related to your cell construction. Are you adding UIImageView on every cellForRowAtIndexPath message?
I guess you have something similar to:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"cellName";
myCell *cell = (myCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
//CONSTRUCT CELL.
//THIS IS THE ONLY PLACE WHERE YOU SHOULD ADD SUBVIEWS TO YOUR CELL
}
//Are you adding subviews here? -you shouldn't
//configure data in cell
return cell;