There is a problem with reusing cells in UITableView:
lets say I have a http request in a cell which may take about 20 seconds to execute. Its completion block effects the cell appearance. User might want to scroll the tableview at this time (while request is executing). After the request has been executed, completion block will effect not a proper cell, because the cells are reused while tableview is scrolling.
Also if you just make a check in the completion block that the cell is the proper one (for example by tagging each cell and checking if it is the same) you will not see a change in the cell appearance when you scroll back to the proper cell.
Do you have any idea of an elegant solution for the problem?
Here is a simplified example of my code which has this problem. It is an IBAction method for a button in a cell which may change the cell by method setInviteStatus
- (void)setInviteStatus:(INVITE_STATUS)status withOldTag:(NSInteger)oldTag {//old tag is to change only if cell is related to the invitation, because tableView reuses cells
if (self.tag != oldTag)
return;
switch (status) {
case INVITE_STATUS_ACCEPTED:
[self.btnInvite setTitle:#"Accepted" forState:UIControlStateNormal];
break;
case INVITE_STATUS_INVITE:
[self.btnInvite setUserInteractionEnabled:YES];
[self.btnInvite setTitle:#"Invite" forState:UIControlStateNormal];
break;
case INVITE_STATUS_REJECTED:
[self.btnInvite setTitle:#"Rejected" forState:UIControlStateNormal];
break;
default:
break;
}
}
- (IBAction)invitePressed:(UIButton *)sender {
NSInteger tag = self.tag; //to save it, it might change due to cell reusing
[[JLAccountManager sharedManager] inviteUser:tag withCompletion:^(NSDictionary *response, NSError *error) {
if ([response[ResponseAnswer] isEqual: #YES]) {
[self setInviteStatus:INVITE_STATUS_ACCEPTED withOldTag:tag];
} else {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setInviteStatus:INVITE_STATUS_INVITE withOldTag:tag];
});
[self setInviteStatus:INVITE_STATUS_REJECTED withOldTag:tag];
}
}];
}
Related
I have an issue with my UIRefreshControl : after the first time it was triggered, it cancels touch on top of my first custom cell (even if it is hidden), like if it was invisible.
If I remove it from superview, then add it again, the issue is still there.
Actually I have a UITableView that is populated with some data. The user can refresh the table view by pulling it and for that I've added an UIRefreshControl to my table view like this in viewDidLoad()
self.refreshControl = [[UIRefreshControl alloc]init];
self.refreshControl.tintColor = [UIColor colorWithRed:0.35 green:0.78 blue:0.98 alpha:1.0];
[_tableView addSubview:self.refreshControl];
I've not added selector to the refreshControl object because I want the tableView to refresh only when the user stopped touching the screen. So for that I've added this ScrollView's delegate method :
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
if (self.refreshControl.isRefreshing) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_main_queue(), ^{
[self refreshTable];
});
});
}
}
And here is my refresh method :
- (void)refreshTable {
//refresh code
[self.refreshControl endRefreshing];
}
I don't want to put my app online with this issue, even if this does not block user to use it !
Thanks in advance for your help !
I've found the solution by going to this post
So effectively in my case I did not have to add a selector, just implement those UIScrollViewDelegate's method :)
In my VC I have a UITableView. Each cell has a UITableView as one of its contents. Timer is set updating each cell every 10secs. Events are handled which also reloads the respective cell.
Method that timer calls :-
-(void) updateVisitotsLists {
NSLog(#"UPDATING VISITORS LIST ***************************");
// Call API's to get lists
[api getVisitorsList];
// Init Arrays
browsingList = [MainAppDataObject sharedAppDataObject].visitors_browsingList;
secondList = [MainAppDataObject sharedAppDataObject].visitors_secondList;
thirdList = [MainAppDataObject sharedAppDataObject].visitors_thirdList;
fourthList = [MainAppDataObject sharedAppDataObject].visitors_fourthList;
// AS these are no more useful, so make it nil & save memory
[MainAppDataObject sharedAppDataObject].visitors_browsingList = nil;
[MainAppDataObject sharedAppDataObject].visitors_secondList = nil;
[MainAppDataObject sharedAppDataObject].visitors_thirdList = nil;
[MainAppDataObject sharedAppDataObject].visitors_fourthList = nil;
// Reload all lists with latest data
[self reloadBrowsingRow];
[self reloadSecondRow];
[self reloadThirdRow];
[self reloadFourthRow];
}
Event Handler Method :-
-(void) handleNewVisitor : (NSNotification *) notification {
// UPDATE VISITOR'S LIST
Visitor *v = [notification object];
#try {
if (v != nil) {
// Add V to browsing list
[browsingList addObject:v];
// Reload browsing list
[self reloadBrowsingRow];
}
}#catch (NSException *e) {
NSLog(#"EXCEP - %#", e);
}
v = nil;
return;
}
Reloading Method -
-(void)reloadBrowsingRow {
// Browsing
VisitorsListsCell *bcell = (VisitorsListsCell*)[self.visitorlistsTv cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]];
[bcell.listsTableView reloadData];
[bcell updateButtonText];
[bcell setNeedsDisplay];
bcell = nil;
return;
}
The Problem :-
When updateVisitotsLists is called thru timer, the updated contents are not reflected on cell.
When event handler method calls the same [self reloadBrowsingRow]; method, the contents of the cell are updated and reflected.
Due to this despite cells contents are updated but are not reflected until the state of cell is changed - expanded or collapsed.
I tried removing timer and cell updates properly on event caught, but when timer was on and event was caught, method is called but contents are not reflected on the screen.
I feel both methods may be calling reload method at same time, hence this must be happening or what ? How can this be handled making sure that the contents of cells are updated in any respect ? Any help is highly appreciated. Thanks.
Use [tableview reloadData]; on that method.
Because Reload on tableView will reload all data, so it will be more helpful.
So self.pic is the UIButton that I want to hide. But it never actually hides it for some reason.
Please Help
- (void)tick:(NSTimer*)time
{
self.pic.hidden = YES;
[NSThread sleepForTimeInterval:1];
self.pic.hidden = NO;
[self.pic setEnabled:NO];
if (self.checkPic == YES)
{
self.lives--;
self.livesLabel.text = [NSString stringWithFormat:#"Lives : %d", self.lives];
}
[self.pic setImage:[self backgroundImageForGame] forState:UIControlStateNormal];
[self check];
self.pic.enabled = YES;
}
Just to elaborate on what #rmaddy said in his comment, you shouldn't ever sleep the main (or UI) thread. When you do this, you're blocking the thread that is responsible for making the visual changes to the button, like whether or not it is hidden. The solution is to do the waiting asynchronously, and GCD has a built in function that makes this painless.
dispatch_after() allows you to create a block that will be executed after a specified delay, on a queue of your choosing. Since you want to update the UI, you'll want to come back to the main queue to make the changes to the on screen button. Here's an example:
self.pic.hidden = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.pic.hidden = NO;
});
I am using setNeedsDisplay on my GUI, but there update is sometimes not done. I am using UIPageControllView, each page has UIScrollView with UIView inside.
I have the following pipeline:
1) application comes from background - called applicationWillEnterForeground
2) start data download from server
2.1) after data download is finished, trigger selector
3) use dispatch_async with dispatch_get_main_queue() to fill labels, images etc. with new data
3.1) call setNeedsDisplay on view (also tried on scroll view and page controller)
Problem is, that step 3.1 is called, but changes apper only from time to time. If I swap pages, the refresh is done and I can see new data (so download works correctly). But without manual page turn, there is no update.
Any help ?
Edit: code from step 3 and 3.1 (removed _needRefresh variables pointed in comments)
-(void)FillData {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *stateID = [DataManager ConvertStateToStringFromID:_activeCity.actual_weather.state];
if ([_activeCity.actual_weather.is_night boolValue] == YES)
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#_noc", [_bgs objectForKey:stateID]]];
if (_isNight == NO)
{
_bgTransparencyInited = NO;
}
_isNight = YES;
}
else
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#", [_bgs objectForKey:stateID]]];
if (_isNight == YES)
{
_bgTransparencyInited = NO;
}
_isNight = NO;
}
[self.contentBgImage setNeedsDisplay]; //refresh background image
[self CreateBackgroundTransparency]; //create transparent background if colors changed - only from time to time
self.contentView.parentController = self;
[self.contentView FillData]; //Fill UIView with data - set labels texts to new ones
//_needRefresh is set to YES after application comes from background
[self.contentView setNeedsDisplay]; //This do nothing ?
[_grad display]; //refresh gradient
});
}
And here is selector called after data download (in MainViewController)
-(void)FinishDownload:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[_activeViewController FillData]; //call method shown before
//try call some more refresh - also useless
[self.pageControl setNeedsDisplay];
//[self reloadInputViews];
[self.view setNeedsDisplay];
});
}
In AppDelegate I have this for application comes from background:
-(void)applicationWillEnterForeground:(UIApplication *)application
{
MainViewController *main = (MainViewController *)[(SWRevealViewController *)self.window.rootViewController frontViewController];
[main UpdateData];
}
In MainViewController
-(void)UpdateData
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(FinishForecastDownload:) name:#"FinishDownload" object:nil]; //create selector
[[DataManager SharedManager] DownloadForecastDataWithAfterSelector:#"FinishDownload"]; //trigger download
}
try this:
[self.view performSelectorOnMainThread:#selector(setNeedsLayout) withObject:nil waitUntilDone:NO];
or check this link:
http://blackpixel.com/blog/2013/11/performselectoronmainthread-vs-dispatch-async.html
setNeedsDisplay triggers drawRect: and is used to "redraw the pixels" of the view , not to configure the view or its subviews.
You could override drawRect: and modify your labels, etc. there but that's not what it is made for and neither setNeedsLayout/layoutSubviews is.
You should create your own updateUI method where you use your fresh data to update the UI and not rely on specialized system calls meant for redrawing pixels (setNeedsDisplay) or adjusting subviews' frames (drawRect:).
You should set all your label.text's, imageView.image's, etc in the updateUI method. Also it is a good idea to try to only set those values through this method and not directly from any method.
None of proposed solutions worked. So at the end, I have simply remove currently showed screen from UIPageControllView and add this screen again. Something like changing the page there and back again programatically.
Its a bit slower, but works fine.
My table view has 2 sections where in the first section has a switch and other section has couple of table view cells. When the switch if on, the cells should be visible and selected and when the switch is off the cells should be hidden.
If my cellForRowAtIndexPath for my first section I execute a selector when the user changes the switch state as per below:
- (UITableViewCell *)cellForRowAtIndexPathForFirstSection
{
[self.tableViewFirstCell.mySwitch addTarget:self action:#selector(changeState:) forControlEvents:UIControlEventValueChanged];
return self.tableViewFirstCell;
}
changeStateMethod:
- (void)changeState:(UISwitch *)sender
{
if ([sender isOn])
{
//Show other section and select its cells.
}
// Reload the table view.
[self.tableView reloadData];
if (![sender isOn])
{
// Collect the selected cells data and hide the section.
}
}
There are scenarios when the user lands on the view for the first time, the system should handle changing the state of the switch. If the switch should be made ON I have:
[self.tableViewFirstCell.mySwitch setOn:YES animated:YES]; or to make it OFF I have:
[self.tableViewFirstCell.mySwitch setOn:NO animated:YES];
QUESTION:
So whether the switch is changed by the system or by the user, the logic to be executed after changing the state of the switch is same in both the cases. In short, when the user changes the switch the selector method gets called, but when the system changes the state is there a way I can call the selector which figures out what state is the switch changed to and then execute the logic appropriately as it does when the user changes the switch?
For example: When the user changes the switch state to ON, logic inside "if ([sender isOn])" gets executed. I want to execute this logic when the system changes the state of the switch to ON.
The delegate methods are only called when a user interacts with the control. If you want the same event processed when you change the state via code, simple call the event handler yourself.
[self.tableViewFirstCell.mySwitch setOn:NO animated:YES];
[self changeState:self.tableViewFirstCell.mySwitch];
That's it. Nice and simple.