Animate UITableViewCells with repeat and scroll - ios

I have a simple table view, I'm using custom cells with one UIImage and some labels.
I want to animate the image size, for that I've added the following in the cellForRowAtIndexPath method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"customCell";
NotificationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[NotificationTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
if( [[[notification objectAtIndex:[indexPath section]] valueForKey:#"readed"] isEqualToString:#"readed"])
{
[UIView animateWithDuration:0.9
delay:0.3
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionRepeat
animations:(void (^)(void)) ^{
cell.imgIcon.transform=CGAffineTransformMakeScale(0.8, 0.8);
}
completion:^(BOOL finished){
cell.imgIcon.transform=CGAffineTransformIdentity;
}];
}
return cell;
}
When I launch the app it works ok, only the "readed" cells are animating in a bucle with the UIViewAnimationOptionRepeat option.
The problem begins when i scroll over the tableview. Some "readed" cells stops animating, and the cells animation time loop is different in each cell.
Any idea?
Thanks
EDITED: I think that the problem is something about reusable cells.

You said "I try reloadind the tableview but this dont work." (sic)
You tried reloading the table view when and how? In response to a
UIApplicationWillEnterForegroundNotification message handler being called? Post that code.
I would add a log statement in your cellForRowAtIndexPath method to make sure it's being called, and log the indexPath(s) that are being updated.
Are you sure you want the images in every cell of your table view to animate every time your app comes back to the foreground. That seems like it would be annoying.

Reloading the tableView should cause the animation to occur, as Duncan has said. Make sure you breakpoint/log the animation to make sure that the code is actually getting there. Once you figure out how to cause the animation to occur again by reloading the table, you can add two NSNotificationCenter callouts to let the app know to reload the table once the app resumes activity from the background.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
Just set up the proper selectors to handle and reload your table:
e.g
- (void)appDidBecomeActive:(id)sender {
[_tableView reloadData];
}

Related

Why UITableView reloadData can not in animate block?

Before, I don't know why, when I reloadData my tableVIew, it takes a very long time.
I have test Log to prove, shows the slow is not because the network:
2016-12-29 14:50:20.958 Eee[1572:25220] lml-info-vc-test-net-began
2016-12-29 14:50:20.958 Eee[1572:25220] lml-info-vc-test-net-end
2016-12-29 14:50:20.972 Eee[1572:25220] lml-info-vc-test-net-animations-reloadData-bigin
2016-12-29 14:50:34.870 Eee[1572:25220] lml-info-vc-test-net-animations-reloadData-end
As we see, the net-began and net-end takes very little time.
But the reloadData-bigin and reloadData-end takes a long time, so I searched the SO, what did the reloadData do? I want to know deeply, not simple answer, I searched always is simple answer, not analyse in depth.
My reloadData code:
//[self.pre_tableView reloadData];
[UIView animateWithDuration:0 animations:^{
NSLog(#"lml-info-vc-test-net-animations-reloadData-bigin");
[self.pre_tableView reloadData];
NSLog(#"lml-info-vc-test-net-animations-reloadData-end");
} completion:^(BOOL finished) {
//Do something after that...
[_pre_tableView.mj_footer endRefreshing];
[_pre_tableView.mj_header endRefreshing];
}];
I use animation block, and in the completionHadler to end the refreshing.
I also searched the apple docs for reloadData
In the Discussion:
Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible. It adjusts offsets if the table shrinks as a result of the reload. The table view’s delegate or data source calls this method when it wants the table view to completely reload its data. It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates and endUpdates.
Attention
With the attentive sentence:
It should not be called in the methods that insert or delete rows, especially within an animation block
Well! It means I should not in my UIView's animateWithDuration method to reloadData, so I replace my code to below:
[self.pre_tableView reloadData];
[_pre_tableView.mj_footer endRefreshing];
[_pre_tableView.mj_header endRefreshing];
Now it is not slow any more. Very happy, I find the reason.
But, I just want to know why, why can not put reloadData method in animation block ?
And it reloadData did not fail, even takes a long time, in the animate block happens what? then it takes so many time here?
Edit -1
My additional code is below:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.pre_dataSource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:info_TableIdentifier];
if (cell == nil) {
cell = [[LMLAgricultureTechCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:info_TableIdentifier];
}
//NSLog(#"lml-info-vc-test-cellSetModel-began");
((LMLAgricultureTechCell *)cell).model = self.pre_dataSource[indexPath.row];
//NSLog(#"lml-info-vc-test-cellSetModel-end");
((LMLAgricultureTechCell *)cell).indexPath = indexPath;
((LMLAgricultureTechCell *)cell).delegate = self;
((LMLAgricultureTechCell *)cell).photo_view.delegate = self;
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// >>>>>>>>>>>>>>>>>>>>> * cell自适应 * >>>>>>>>>>>>>>>>>>>>>>>>
id model;
model = self.pre_dataSource[indexPath.row];
return [self.pre_tableView cellHeightForIndexPath:indexPath model:model keyPath:#"model" cellClass:[LMLAgricultureTechCell class] contentViewWidth:[self cellContentViewWith]];
}
UIView.animateWithDuration... methods can only animate values that are animatable. Although it sometimes feels like magic - it's not really...
reloadData is an async method that can not be handled by the animate block the be animated.
If you want to animate the transition you can either use insert/delete/moveRows or use the transitionWithView method of UIView. This method would render the view off-screen completely as it will look after all the changes you put in it's block, than animate transit between the current state of the view and the newly rendered view. The animation itself depends on the options you deliver, and you probably want to use UIViewAnimationOptionsTransitionCrossDissolve.
[UIView transitionWithView:self.pre_tableView
duration:0.3
options:UIViewAnimationOptionsTransitionCrossDissolve
animations: ^{
[self.pre_tableView reloadData];
} completion: ^(BOOL finished) {
...
}];

How to make sure changing collectionview cells appear at the center of the phone screen?

Scenario: I have a collection view that is reloaded every 15 seconds. There is no limit to the amount of cells that could be in this collection view, however, only one cell is highlighted at a time. There is only one section, and the collectionview scrolls horizontally. I need to make sure the highlighted cell is always in the center of the phone screen. For example, if the 24th cell is highlighted, it would be a bad user experience to have to scroll all the way until you find it. However, a completely different cell could be highlighted when the collection view gets reloaded in another 15 seconds.
See bottom portion of the image for a better idea of highlighted and unhighlighted cells.
Here's an idea of what I've tried that pertains to the index path of the highlighted cell and making sure it's in the center of the phone screen.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
EZPlayerCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"PlayerCellID" forIndexPath:indexPath];
NSDictionary *rowData = [[_gameBoardDictionary objectForKey:#"players"] objectAtIndex:indexPath.row];
if ([[rowData objectForKey:#"possession"] integerValue] == 1) {
cell.isHighlighted = YES;
} else {
cell.isHighlighted = NO;
}
if (cell.isHighlighted) {
self.highlightedCellIndexPath = [self.collectionView indexPathForCell:cell];
} else {
}
return cell;
}
- (void)viewDidLayoutSubviews {
[self.collectionView scrollToItemAtIndexPath:self.highlightedCellIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
I've tried a number of things, but this should provide a reference for my train of thought. Any help would be appreciated. Thank you so much!
Please add a observer for the "contentSize" keypath of the collection view.
[self.collectionView addObserver:self forKeyPath:#"contentSize" options:NSKeyValueObservingOptionOld context:NULL];
You can then move the scrolling code inside the observeValueForKeyPath method.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)changeDict context:(void *)context
{
[self.collectionView scrollToItemAtIndexPath:self.highlightedCellIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
Here's what solved the issue for me. Instead of trying to get the indexPath in cellForItemAtIndexPath, I got the indexPath based on my data and then scrolled to the cell at that indexPath.
NSArray *players = [_gameBoardDictionary objectForKey:#"players"];
self.itemNumber = [players indexOfObjectPassingTest:
^BOOL(NSDictionary *dict, NSUInteger idx, BOOL *stop)
{
return [[dict objectForKey:#"possession"] isEqual:#1];
}
];
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:self.itemNumber inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];

Wrong object added to UITableView

In my app I get an object by NSNotificationCenter (form another controller) and add the object to UITableView:
-(void)viewWillAppear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(RosterSave:) name:#"RosterSave" object:nil];
}
-(void)RosterSave:(NSNotification *)notification
{
NewRoster* newRoster = [[NewRoster alloc]init];
newRoster = notification.object;
[myUser.rosterArray addObject:newRoster];
[self.myRoster reloadData];
}
This is the tableView method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return myUser.rosterArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *iden = #"MyTable";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:iden];
if (cell == nil)
{
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:iden];
}
NewRoster* myNewRoster = [myUser.rosterArray objectAtIndex:indexPath.row];
cell.textLabel.text = myNewRoster.nameRoster;
return cell;
}
When the user adds the first object, the tableView get own row. When the user adds the second object, it adds two rows of the second object and on this way.
How can I fix this issue?
You have add observer(notification) in viewWillAppear which get called everytime when view will appear.
add notification in viewDidLoad instead of viewwillAppear.
I always like to put NSNotification subscriptions in init / and unsubscriptions in dealloc. This pattern is easy to read and debug. Also, it guarantees you will never double subscribe or double unsubscribe.
In your case, you are prone to creating multiple subscribtions in viewWillAppear
- (instancetype)init
{
...
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(RosterSave:) name:#"RosterSave" object:nil];
...
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#Feroz is right about you allocating a new object and replacing it with notification.object. #Lion is right about viewDidLoad vs. viewDidAppear You are generating multiple notifications. You need to only generate one per object. Put a breakpoint in your RosterSave code and count how many times it's called per new object. Also look at the stack trace to see who is generating these calls. It's down to a simple matter of stepping through, understanding your code, and seeing what's happening.

UITableView reloadData not working on MainTable in Split View Controller

I have a feature in my app where the user can change the color scheme of the app. The app uses a Split View Controller, with a MainTable and DetailView table. Everything works fine except for the MainTable. What is failing is that the MainTable reloadData method is not causing the cells to be redrawn.
It should be noted that I am changing globalHighContrast and sending the notification from a UIModalPresentationFormSheet viewController, so the tables are kind of visible on the screen while the viewController is active.
I am triggering the screen update from a notification, like this:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reloadAllTables)
name:#"contrastModeChanged"
object:nil];
Then, to make sure that I call reloadData on the main thread, I am handling the notification like this:
-(void)reloadAllTables{
[self performSelectorOnMainThread:#selector(doReloadAllTables) withObject:nil waitUntilDone:NO];
}
-(void)doReloadAllTables{
[self showIcon];
if( globalHighContrast ){
theTable.backgroundColor = [Colors lightBkgColor];
self.view.backgroundColor = [Colors lightBkgColor];
} else {
theTable.backgroundColor = [Colors darkBkgColor];
self.view.backgroundColor = [Colors darkBkgColor];
}
[detailViewController configureView:currentMainMenu];
[detailViewController.subTable reloadData];
[theTable reloadData];
// desperate try to force it to work
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentMainMenu inSection:0];
[self tableView:theTable didSelectRowAtIndexPath:indexPath];
}
Both reloadAllTables and doReloadAllTables are being called, but
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
is not being called.
As soon as I tap a cell on the MainTable it does update correctly to the new color scheme.
Also, there is a desperate attempt to workaround this by trying to simulate the MainTable touch, but that doesn't work either.
You can try to put code for updating you scheme in -(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath method...

Where to addObserver to NSNotificationcenter in a custom UITableViewCell?

In my UITableViewCell I have a method initNotification which is called by the TableViewController in cellForRowAtIndexPath where the TableCells are created.
My Problem is that, every time this view is reloaded, the initNotification method is called again, so when the Notification appears, the NotificationHandle is called x-times!
I have tried to remove the Observer before adding it again with:
-(void) initNotification{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(handleNotificationOnOff:)
name:[[NSString alloc] initWithFormat:#"%#",[self.light beckhoffOnOff]]
object:nil];
}
but this do not work either.
The Problem is, I cannot use a bool-flag or anything like that, because the Cells are always reinitialized by the ViewController.
Is there a proper way to remove the NotificationHandle form the NotificationCenter?
edit: This is how I create my custom TableViewCells
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
Light* l = [[staticModel.model getRoomAtIndex:[indexPath section]]getLightAtIndex:[indexPath item]];
if([l typ]==ONOFF){
TableCellLight *conof = [tableView dequeueReusableCellWithIdentifier:#"ReuseIDOnOff" forIndexPath:indexPath];
LightOnOff *lonof = (LightOnOff*) l;
[[conof label] setText: [lonof bezeichnung]];
conof.light=lonof;
[conof initNotification];
cell = conof;
}
if([l typ]==DIMMER){
TableCellLightDim *cdim = [tableView dequeueReusableCellWithIdentifier:#"ReuseIDDim" forIndexPath:indexPath];
LightDim *ldim= (LightDim*) l;
[[cdim label] setText: [ldim bezeichnung]];
[[cdim slider]setValue:[ldim dimVal]];
cdim.light=ldim;
[cdim initNotification];
cell = cdim;
}
if([l typ]==RGB){
TableCellLightRGB *crgb = [tableView dequeueReusableCellWithIdentifier:#"ReuseIDRGB" forIndexPath:indexPath];
LightRGB *lrgb= (LightRGB*) l;
[[crgb label] setText: [lrgb bezeichnung]];
crgb.light=lrgb;
crgb.owner=self;
[crgb initNotification];
cell = crgb;
}
return cell;
}
Thanks
Generally speaking the cell shouldn't be observing anything. The controller should be observing changes and pushing the updated information onto the cells.
Calling removeObserver: before adding the observer should work. If you were going to do anything in prepareForReuse or tableView:didEndDisplayingCell:forRowAtIndexPath: to reset the cell, that would be the code you use. You need to look at how you tested that it wasn't working and how you're reusing cells.

Resources