Is there an easy way to trigger a [UITableView reloadData] from within a UITableViewCell? I am loading remote images to be displayed after the initial table display, and updating the image with self.image = newImage does not refresh the table. Resetting the cell's text value does refresh the table, but this seems sloppy.
MyTableViewCell.h
#interface MyTableViewCell : UITableViewCell {}
- (void)imageWasLoaded:(ImageData *) newImage;
MyTableViewCell.m
#implementation MyTableViewCell
- (void)imageWasLoaded:(UIImage *) newImageData {
self.image = newImage; //does not refresh table
//would like to call [self.tableView reloadData] here,
//but self.tableView does not exist.
//instead I use the below line
self.text = self.text; //does refresh table
}
#end
I did the exact thing that you're trying to do. The thing you're looking for is needsLayout. To wit (this is a notification observer on my UITableViewCell subclass):
- (void)reloadImage:(NSNotification *)notification
{
UIImage *image = [[SSImageManager sharedImageManager] getImage:[[notification userInfo] objectForKey:#"imageUrl"];
[self setImage:image];
[self setNeedsLayout];
}
This will pop in your image without having to reload the entire table, which can get very expensive.
Get a reference to the containing UITableView using the superview property. Then tell it to "reloadData":
UITableView *parentTable = (UITableView *)self.superview;
[parentTable reloadData];
It is less expensive to only reload the sections/rows that need reloading. In iPhone SDK 3.0 there are some methods for doing just that.
Hmm. So imageWasLoaded: is a delegate method called by some asynchronous image loading code? Actually it seems weird that setting self.image does not update the image. Have you tried to add your own UIImageView to the cell rather than using the image property? Might be kind of a hack, but this way the image should update right away (without you having to reload the whole table view, which is definitely not desireable).
Even better: If you are using SDK 3.0, you can use the new imageView property in UITableViewCell (I have not tried this, though):
self.imageView.image = newImage;
Actually, I needed to do exactly the same some weeks ago. However, my approach was to subclass UIImageView and do all the asynchronous image loading/updating in that class. The main reason I did it this way was that I wanted to have a generic and reusable component which can be used in different table view cells (or elsewhere).
CodeGrue is right. After days thinking why [subclassOjb.tableView reloadData] wasn't working I gave up trying to understand that and settled for the only two methods I know works and it's very easy to implement: 1- Notification Center (from tableviewcell you shouldn't use this one) 2- or use the superview to get a handle on your tableview property (this is perfect for uitableviewcells).
Note that using the superview method will work on your uitableviewcell class only, chances are if you have a custom cell you will have a uitableviewcell class which the superview is your tableviewcontroller or whateverClass you have with a uitableview delegate + datasource.
Now if you need to reload your tableview from another class, a datasource class let's say a singleton or whatever; you should use a block within your tableview class to load the data on a different thread, with an inner block to reload the table on the mainthread.
//handle tablview to show spinner (you have to lower your tableview out of the way yourself if you are calling this method)
[self.tableView setContentOffset:CGPointMake(0, -rf.frame.size.height) animated:YES];
//start spinner animation here
[rf beginRefreshing];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
// Load Data from data source class here
[myDataSourceClass fetchDataMethod];
dispatch_async(dispatch_get_main_queue(), ^{
//Reload data and stop spinner
[self.tableView reloadData];
[rf endRefreshing];
//handle tablview to hide spinner atrributed title
[self.tableView setContentOffset:CGPointMake(0, 0) animated:YES];
});
});
If you are on a different view and something happens that you need to reload your table on your table view. Well at that point your table is not on screen there is no reason why you should reload your table if no one is going to see it. Just add [self.tableview reloadDAta] to your viewWillAppear method when users go back to your tableviewcontroller view it will reload the table on the fly.
Anyway for tableviewcell just use the following inside your action to reloadData (credit to CodeGrue)
UITableView *parentTable = (UITableView *)self.superview;
[parentTable reloadData];
**different topic but for those of you starting now. This will save you from possible headaches -
after creating your tableclass and before you even start to deal with it's delegate methods - do this: in your viewDidLoad add the following code:
self.tableView.delegate = self;
self.tableView.dataSource = self;
Anybody that was looking for a way to reload the table from the cell and figured out that CodeGrue's answer does not work, you can still do it this way but you need to double check.
Disclaimer
You should always use a delegate pattern for this, what follows is a hack that might break again in future versions.
Hack to reload the data from the cell
UITableView *parentTable = (UITableView *)self.superview;
if (![parentTable isKindOfClass:[UITableView class]]) {
parentTable = (UITableView *) parentTable.superview;
}
[parentTable reloadData];
try [yourtableview reloadData];
after setting the image to a new image
Related
I have an app that is using UIKit state preservation in iOS 6. I am able to save/restore the state of the view controllers, ie which tab is selected and navigation controller hierarchy, however I cannot get my table view to restore it's offset. I have a restoration identifier in my storyboard for the view as well as the view controller and the view controller (the table's data source) implements UIDataSourceModelAssociation as follows:
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view
{
TSStatus *status = [self._fetchedResultsController objectAtIndexPath:indexPath];
return status.objectID.URIRepresentation.absoluteString;
}
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
{
NSURL *statusURL = [NSURL URLWithString:identifier];
NSManagedObjectID *statusID = [[TSDataController sharedController].persistentStoreCoordinator managedObjectIDForURIRepresentation:statusURL];
TSStatus *status = (TSStatus *)[[TSDataController sharedController].mainContext objectWithID:statusID];
return [__fetchedResultsController indexPathForObject:status];
}
modelIdentifierForElementAtIndexPath:inView: is getting called when the app goes into the background, however modelIdentifierForElementAtIndexPath:inView: never gets called.
This isn't a real answer to your question, but I've not been able to get a table view restore its contentOffset, either.
I guess this is a bug in iOS 6, because the documentation clearly states that a UITableView restores its contentOffset, when 1) it has a restorationIdentifier 2) the view controller the view belongs to has a restorationIdentifier and 3) the data source conforms to the UIDataSourceModelAssociation protocol.
You can restore the contentOffset and the selected item manually in your view controller, though:
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder];
[coder encodeObject:[NSValue valueWithCGPoint:self.tableView.contentOffset] forKey:#"tableView.contentOffset"];
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
if (indexPath != nil) {
NSString *modelIdentifier = [self modelIdentifierForElementAtIndexPath:indexPath inView:self.tableView];
[coder encodeObject:modelIdentifier forKey:#"tableView.selectedModelIdentifier"];
}
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
[super decodeRestorableStateWithCoder:coder];
CGPoint contentOffset = [[coder decodeObjectForKey:#"tableView.contentOffset"] CGPointValue];
self.tableView.contentOffset = contentOffset;
NSString *modelIdentifier = [coder decodeObjectForKey:#"tableView.selectedModelIdentifier"];
if (modelIdentifier != nil) {
NSIndexPath *indexPath = [self indexPathForElementWithModelIdentifier:modelIdentifier inView:self.tableView];
if (indexPath != nil) {
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
}
I have no idea why UITableView doesn't do that automatically, even though the documentation says it does. If someone knows the answer, please comment.
I've found this can work, if the UITableView also has a restorationIdentifier set.
However it doesn't work if the UITableViewController is inside a UINavigationController. This has been reported to Apple, Problem ID: 13536778. This problem seems to occur on both iOS 6.0 and 6.1.3.
This is a bug in iOS 6.
To get state restoration for your table view working using the UIDataSourceModelAssociation protocol you should call -reloadData on your table view before returning the valid index path in -indexPathForElementWithModelIdentifier:inView: like so:
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
{
NSURL *statusURL = [NSURL URLWithString:identifier];
NSManagedObjectID *statusID = [[TSDataController sharedController].persistentStoreCoordinator managedObjectIDForURIRepresentation:statusURL];
TSStatus *status = (TSStatus *)[[TSDataController sharedController].mainContext objectWithID:statusID];
[self.tableView reloadData];
return [__fetchedResultsController indexPathForObject:status];
}
See Apple's State restoration sample for how to achieve this. The magic fix happens in the decodeRestorableStateWithCoder method with a call to reloadData:
MyTableViewController.m
// this is called when the app is re-launched
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
// important: don't affect our views just yet, we might not visible or we aren't the current
// view controller, save off our ivars and restore our text view in viewWillAppear
//
NSLog(#"MyTableViewController: decodeRestorableStateWithCoder");
[super decodeRestorableStateWithCoder:coder];
self.tableView.editing = [coder decodeBoolForKey:kUnsavedEditStateKey];
[self.tableView reloadData];
}
Note it is odd that they encode the editing state because editing is ended by their enter background notification handler before preservation begins so it will always restore not editing. Also they try to set self.tableView.editing instead of self.editing so the edit button wouldn't update. Also note the comment about not affecting the views which is odd firstly given they do affect the views and second viewWillAppear is called before decoding state. Given these mistakes I wouldn't use this example to tune your programming skills.
The other answer states to reload in the indexPathForElementWithModelIdentifier which is not a good idea given it is called multiple times (at least twice) to find various index paths of visible and selected objects.
e.g as you are watching the tableView having cell with plus Button, when I press that button it hides the current button but when I scroll the tableview Some other button on other customCells are also get hide but I didn't hide them. please help me out. How to fit this problem.
The UITableView is caching the cells, which means it doesn't store all cells at all time, which is brilliant in terms of memory. The problem with it, that it has no reference to the value stored in that cell when the cell is reused. What is often done, is that all values stored in a dynamic cell is stored in separate NSMutableArrays.
For your problem, you could add an array with the boolean values indicating whether they are hidden or not, and read from that in the tableView:cellForRowAtIndexPath: delegate method with
cell.hidden = [[self.yourArray objectAtIndex:indexPath.row] boolValue];
And in the button callback method you should change the hiddenproperty as well as updating the value in the array.
i'll do it as follow:
first you have to track the button's state:
Shown
or
Hidden
this is done by holding the state in an NSMutableArray
in the viewDidLoad method add the following
NSMutableArray *shownButtons = [[NSMutableArray alloc] init];
then in your tableView:cellForRowAtIndexPath do the following
NSString *tmpIndexPathString = [NSString stringWithFormat:#"%d",indexPath.row];
if ([shownButtons containsObject:tmpIndexPathString])
{
[cell.myButton setHidden:YES];
}
else
{
[cell.myButton setHidden:NO];
}
In the tableView:cellForRowAtIndexPath: delegate method you need to loop through all the visible cells:
for (UITableViewCell *cell in [self.tableView visibleCells]) {
// now you have a cell that you can update
}
You also have to remember to update your data source so scrolling the table will update the cells accordingly and not show the plus button. If you're updating your data source what you can do is reload the cell in the above for loop for example.
I have a UITableView with several UITableViewcells (UITableViewCellStyleSubtitle). When the user taps on one of cells, I push a detail VC that allows the user to change the text of the cell. When I pop this ViewController, the text on the cell appears abbreviated with an ellipsis:
Before:
After:
After changing the text to "Wartyrl" and popping the detail VC.
If I tap on any other cell, the ellipsis disappears and the correct text is displayed. What might be causing this?
This is the code that changes the text of the cell (via a delegate method):
#pragma mark - FRREditTaskViewControllerDelegate
-(void) editTaskViewController:(FRREditTaskViewController *)sender
didChangeNameForTask:(FRRFlatTask *)aTask{
NSIndexPath *idx = [NSIndexPath indexPathForRow:[self.pendings indexOfObject:aTask] inSection:0];
[self.tableView cellForRowAtIndexPath:idx].textLabel.text = aTask.name;
}
BTW, this code is called before popping the detail VC and while the UITableViewController is still hidden. I don't know if this is relevant.
When the cell is created, the UILabel is only large enough to handle the initial text (it doesn't dynamically resize). Calling [self.tableView reloadData] after changing the text (end of your didChangeNameForTask method) should make it display properly.
You can call [cell.textLabel sizeToFit].
In many cases it's cleaner to instead update your internal data and call reloadRowsAtIndexPaths:withRowAnimation:. That way you don't have so many pieces monkeying with the implementation details of the cell (when you change your mind on how to lay it out). It also ensures that your real data matches the cell.
Even better in most cases is to create a UITableViewCell subclass called TaskTableViewCell. You hand it a Task and it observes the Task with KVO, managing its own layout (including calling sizeToFit when needed). That way you don't need delegate methods to tell you when it updates. When the task changes, the cell automatically updates itself. This moves all the layout issues into the cell class and out of the datasource.
It seems that the textLabel hesitates to resize its width. If its too adamant to change, just reload the tableView directly using [tableView reloadData].
If you don't want to reload the entire tableView, you can reload just a particular cell like that following.
NSIndexPath *idx = [NSIndexPath indexPathForRow:[self.pendings indexOfObject:aTask] inSection:0];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:idx] withRowAnimation:UITableViewRowAnimationFade];
This will show a nice fade animation when changing the textLabel's text of the cell.
-(void) editTaskViewController:(FRREditTaskViewController *)sender
didChangeNameForTask:(FRRFlatTask *)aTask{
NSIndexPath *idx = [NSIndexPath indexPathForRow:[self.pendings indexOfObject:aTask] inSection:0];
UITableViewCell *thiscell=[self.tableView cellForRowAtIndexPath:idx];
thiscell.textLabel.text =[nsstring stringwithFormat:#"%#",aTask.name];
[self.tableView reloadRowsAtIndexPaths: [NSArray arrayWithObject:idx] withRowAnimation:NO];//if u dont want to use reload data then use like this or use
//- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
}
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;
After parsing JSON data in a Data class, I set the UIViewController's NSArray *headlines property in a fillArrays method of the same Data class. In the viewDidAppear method of my UIViewController, I call reloadData on my UITableView. numberOfSectionsInTableView fires and returns 1, then numberOfRowsInSection fires and returns an array count of 4 (for 4 strings in the array). However, control never gets to cellForRowAtIndexPath and I'm having the hardest time understanding why, especially since I have valid sections and rows. The cells are all visible.
I've added the UITableViewDataSource and UITableViewDelegate protocols to the UIViewController interface and set the UITableView's delegate and dataSource to self in viewDidLoad (which also is verified by the row and section count methods being called).
I'm wondering if it has something to with me reinitializing the UIViewController in Data.m in order to set its properties.
In Data.m:
- (void)fillArrays:(NSArray *)jsonObjs {
NSLog(#"fillArrays");
HeadlinesRootViewController *hrvc = [[HeadlinesRootViewController alloc] init];
hrvc.headlines = [self getJsonValuesForKey:#"headline" inArrayOfObjects:jsonObjs];
[hrvc viewDidAppear:NO];
}
In ViewController.m:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"viewDidLoad");
// Table view
headlineTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 180, self.view.bounds.size.width, 300) style:UITableViewStylePlain];
[headlineTableView setDelegate:self];
[headlineTableView setDataSource:self];
// Temporary
self.headlines = [[NSMutableArray alloc] initWithObjects:#"headline1", #"headline2", #"headline3", #"headline4", nil];
[self.view addSubview:headlineTableView];
self.headlineTableView = headlineTableView;
[headlineTableView release];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"viewdidappear");
NSLog(#"headlines: %#", self.headlines); // Returns an array of 4 headlines
if( [self.headlines count] != 0 ){
[self.headlineTableView reloadData];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSLog(#"numberOfSectionsInTableView: 1");
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSLog(#"numberOfRowsInSection: %d", [self.headlines count]);
return [self.headlines count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"cellForRowAtIndexPath");
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
}
cell.text = [[NSString alloc] initWithFormat:#"%#", [self.headlines objectAtIndex:indexPath.row]];
return cell;
}
In fillArrays, you create another view controller - but you never do anything with it or its view, you would never see that view. You would never call viewDidAppear manually either, that happens automatically when a view controllers view is displayed (ONLY in the context of a navigation controller though).
Normally the flow is, you create a view controller and either add that view as a subview of a current view, or push it as a new window via a navigation controller. I'm pretty sure your whole issue is that they table is never added to a view anyone actually sees, so the table calls the other methods but never calls cellForRow because its layoutSubviews code is simply not being called.
Have you added your tableView to the view of UIViewController?
It happened to me, and when I added this
[self.view addSubview:table];
[table release];
then cellForRowAtIndexPath started working.
For Google's sake:
If tableView:numberOfRowsInSection returns zero for whatever reason tableView:cellForRowAtIndexPath will not get called because there are no rows to call it for.
Check to make sure that the tableView delegate and dataSource are pointed to the viewController
I cannot see anything wrong with the code as-is, have you verified with breakpoints that cellForRow is never reached (even though I see you have a log statement)?
Also I would try just for a sanity check to return "1" explicitly in rowsInSection, and hardcode a string in the cell you are returning in cellForRow.
If all else fails, create a new table view controller from the XCode templates and put your calls in there - then when that works, work backwards to why your code does not.
Also, it would be good to see your viewDidLoad setup code (add to answer above please).
if you're setting the delegate and datasource at viewDidLoad, then that may be the source of your bug. Can you set the datasource and delegate in init?
I'm not sure that you add your UITableView as subview to UIViewController.view. This was my approach anyway.
In this approach, I found execution did not get into cellForRowAtIndexPath until I sent UIViewController.view to the back after adding UITableView as subview.
Getting this far was only part of the problem. At this point, it seemed that my other view controllers no longer respond to touch events. I found that when I also add the UITableView as a subview to the rootViewController, all my views got the appropriate touch events.
Thank you so much pxl. When I move the UITableView initialization from viewDidLoad to:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil"
it works perfectly when ever I make delete or update some rows the UITableView gets reloded to my UIView.
Swift version
Add self.table.layoutIfNeeded() and then self.tableView.reloadData()