UITableView delay reloadData until animation complete - ios

I've got a UITableView that represents a checklist of items. Each item is in a done/undone state.
I'd like to keep all of the done items at the top of the list, so when the user clicks an item that is undone, I'd like to figure out where the row should go (at end of currently-done items list) and move it there.
I'm trying to use -moveRowAtIndexPath:toIndexPath: for my UITableView to do this.
It works well sometimes and not so well at other times.
It seems to work well when the done action kicks off another animation elsewhere on screen. For some reason, this seems to serve as a delay for the -reloadData call.
When that's not true (i.e., the only thing "happening" is the row being marked done and moving), the animation seems to get short-circuited by an automatic call to the UITableView's -reloadData method. That is, the animation begins, but about halfway through, -reloadData is called and the rows snap to their final position. It's fairly jarring from the user's perspective.
I've tracked through my code to verify that I'm not calling -reloadData myself, and it doesn't appear that I'm the one triggering this -reloadData call.
I'm OK with the automatic call to -reloadData and I understand why it's called (though you'd think it might not be necessary, but that's a different issue), but I'd really like it to wait until it completes its animation.
Here's the code I'm using:
NSIndexPath *oldPath = [NSIndexPath indexPathForRow:currentIndex inSection:0];
NSIndexPath *newPath = [NSIndexPath indexPathForRow:newIndex inSection:0];
[tableView beginUpdates];
[checklist removeObject:task];
[checklist insertObject:task atIndex:newIndex];
[tableView moveRowAtIndexPath:oldPath toIndexPath:newPath];
[tableView endUpdates];
Am I screwing something up?

Swift
//---------
extension UITableView {
// Default delay time = 0.5 seconds
// Pass animation time interval, as a parameter argument
func reloadDataAfterDelay(delayTime: TimeInterval = 0.5) -> Void {
self.perform(#selector(self.reloadData), with: nil, afterDelay: delayTime)
}
}

At the request of titaniumdecoy, here's how I got this issue fixed:
I have a UIViewController subclass with an NSTimer that executes once a second, checking the underlying data source for a UITableView for changes that indicate a call to -reloadData is necessary. So, to that subclass I added:
#property (BOOL) IsAnimating;
I set this initially to NO and if the isAnimating property is set to YES, the NSTimer "short-circuits" and skips its normal processing.
So, when I want to run this UITableView animation, I set the isAnimating property to YES and run the animation.
Then, I schedule a selector to run 1 second in the future that will reset isAnimating to NO. The NSTimer will then continue firing and will see an isAnimating of NO (most likely on the second subsequent call to -codeTimerFired) and then find the data source update, kicking off a call to reloadData.
Here's the code for suspending the NSTimer's processing and scheduling the UITableView animation:
// currentIndex and newIndex have already been calculated as the data item's
// original and destination indices (only 1 section in UITableView)
if (currentIndex != newIndex) {
// This item's index has changed, so animate its movement
NSIndexPath *oldPath = [NSIndexPath indexPathForRow:currentIndex inSection:0];
NSIndexPath *newPath = [NSIndexPath indexPathForRow:newIndex inSection:0];
// Set a flag to disable data source processing and calls to [UITableView reloadData]
[[self Owner] setIsAnimating:YES];
// I haven't tested enough, but some documentation leads me to believe
// that this particular call (-moveRowAtIndexPath:toIndexPath:) may NOT need
// to be wrapped in a -beginUpdates/-endUpdates block
[[[self Owner] InstructionsTableView] beginUpdates];
// These lines move the item in my data source
[[[MMAppDelegate singleton] CurrentChecklist] removeObject:[self CellTask]];
[[[MMAppDelegate singleton] CurrentChecklist] insertObject:[self CellTask] atIndex:newIndex];
// This code is the UITableView animation
[[[self Owner] InstructionsTableView] moveRowAtIndexPath:oldPath toIndexPath:newPath];
// Conclude the UITableView animation block
[[[self Owner] InstructionsTableView] endUpdates];
// Schedule a call to re-enable UITableView animation
[[self Owner] performSelector:#selector(setIsAnimating:) withObject:#(NO) afterDelay:1.0];
} else {
// This location hasn't changed, so just tell my owner to reload its data
[[[self Owner] InstructionsTableView] reloadData];
}
Here's the NSTimer method (note how it bails out if isAnimating == YES):
- (void)codeTimerFired {
// This is a subclass of a template subclass...
// super actually has work to do in this method...
[super codeTimerFired];
// If we're in the middle of an animation, don't update!
if ([self IsAnimating]) {
return;
}
// Other data source processing...
// local BOOL to check whether underlying data source has changed
BOOL shouldUpdate = NO;
// code to check if underlying data source has changed...
// ******************************************************
// [CODE REMOVED]
// ******************************************************
// If the underlying data source has changed, update the UITableView
if (shouldUpdate) {
[self reloadTableView]; // <--- This is the main line I wanted to prevent
// since the code that fired to cause the
// UITableView animation will ALWAYS cause
// the underlying data source to change such
// that this line would fire.
}
}

Related

Why does a strong reference to parent UIViewController in performBatchUpdates leak an activity?

I just finished debugging a very nasty UIViewController leak, such that the UIViewController was not dealloc'd even after calling dismissViewControllerAnimated.
I tracked down the issue to the following block of code:
self.dataSource.doNotAllowUpdates = YES;
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadItemsAtIndexPaths:#[indexPath]];
} completion:^(BOOL finished) {
self.dataSource.doNotAllowUpdates = NO;
}];
Basically, if I make a call to performBatchUpdates and then immediately call dismissViewControllerAnimated, the UIViewController gets leaked and the dealloc method of that UIViewController never gets called. The UIViewController hangs around forever.
Can someone explain this behavior? I assume performBatchUpdates runs over some time interval, say, 500 ms, so I would assume that after said interval, it would call these methods and then trigger the dealloc.
The fix appears to be this:
self.dataSource.doNotAllowUpdates = YES;
__weak __typeof(self)weakSelf = self;
[self.collectionView performBatchUpdates:^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.collectionView reloadItemsAtIndexPaths:#[indexPath]];
}
} completion:^(BOOL finished) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
if (strongSelf) {
strongSelf.dataSource.doNotAllowUpdates = NO;
}
}];
Note that the BOOL member variable, doNotAllowUpdates, is a variable I added that prevents any kind of dataSource / collectionView updates while a call to performBatchUpdates is running.
I searched around for discussion online about whether or not we should use the weakSelf/strongSelf pattern in performBatchUpdates, but did not find anything specifically on this question.
I am happy that I was able to get to the bottom of this bug, but I would love a smarter iOS developer to explain to me this behavior I am seeing.
This seems like a bug with UICollectionView. API users should not expect single-run block parameters to be retained beyond the execution of the task, so preventing reference cycles should not be an issue.
UICollectionView should be clearing up any references to blocks once it has finished the batch update process, or if the batch update process is interrupted (for example, by the collection view being removed from the screen).
You've seen for yourself that the completion block is called even if the collection view is taken off-screen during the update process, so the collection view should then be nilling out any reference it has to that completion block - it will never be called again, regardless of the current state of the collection view.
As you figured out, when weak is not used a retain cycle is created.
The retain cycle is caused by self having a strong reference to collectionView and collectionView now has a strong reference to self.
One must always assume that self could have been deallocated before an asynchronous block is executed. To handle this safely two things must be done:
Always use a weak reference to self (or the ivar itself)
Always confirm weakSelf exists before passing it as a nunnull
param
UPDATE:
Putting a little bit of logging around performBatchUpdates confirms a lot:
- (void)logPerformBatchUpdates {
[self.collectionView performBatchUpdates:^{
NSLog(#"starting reload");
[self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]];
NSLog(#"finishing reload");
} completion:^(BOOL finished) {
NSLog(#"completed");
}];
NSLog(#"exiting");
}
prints:
starting reload
finishing reload
exiting
completed
This shows that the completion block is fired AFTER leaving the current scope, which means it is dispatched asynchronously back to the main thread.
You mention that you immediately dismiss the view controller after doing the batch update. I think this is the root of your issue:
After some testing, the only way I was able recreate the memory leak was by dispatching the work before dismissing. It's a long shot, but does your code look like this by chance?:
- (void)breakIt {
// dispatch causes the view controller to get dismissed before the enclosed block is executed
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]];
} completion:^(BOOL finished) {
NSLog(#"completed: %#", self);
}];
});
[self.presentationController.presentingViewController dismissViewControllerAnimated:NO completion:nil];
}
The code above leads to dealloc not being called on the view controller.
If you take your existing code and simply dispatch (or performSelector:after:) the dismissViewController call you will likely fix the issue as well.

populating tableview datasource inside init method from remote server

I have a tabBarView which have two tableViews. each of these tableViews will represent some news from a remote server. I want to populate tableView's datasource when tableViewController's init method is called. so I have put the needed networking operation inside init method. My init method is this:
- (instancetype) init{
self = [super init];
[NewsManager fetch:10 remoteNewsOfLanguage:#"fa" withOffsett:1 andCompletionHandler:^(NSMutableArray *news) {
self.newsList = news;
}];
self.tabBarItem.title = #"my title";
return self;
}
newsList is an array holding news loaded from server.
But when I run my project the order of invocation for tableViewController's methods is like the following:
tableViewController's init method is called and finished (but the completion handler block is not called yet)
tableViewController's viewDidLoad method is called ( it is called when the tableViewController is added to tabBarView's viewControllers array)
tableViewController's delegate method tableView:numberOfRowsInSection is called
the network operation's completionHandler block is called and the newsList array is set to the retrieved news from server
So my problem is that before my newsList array is populated the method tableView:numberOfRowsInSection is called and so my tableView is not filled with any news. How should I solve this issue?
Thanks
you should reload table data after you get data from server. then only your table will show updated data.
[NewsManager fetch:10 remoteNewsOfLanguage:#"fa" withOffsett:1 andCompletionHandler:^(NSMutableArray *news) {
self.newsList = news;
[yourTableview reloadData];//add this line
}];
The added line does the job and makes the new data to be loaded in the tableView but there is a small point that I think you should consider
[tableView reloadData]
will be executed in a thread other than mainThread and this will cause a 5 to 10 seconds delay for the data to be loaded on the tableView.
to prevent this you should somehow tell it to run the reloadData method on the main thread. this is done with the dispatch_async. So you should call [tableView reloadData] like this:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});

UICollectionView refresh data

I am using a CollectionView which displays an array of objects.
On clicking a button i fill this array with a new set of data.
I want to refresh the CollectionView with this data.
Is there any statement to update this instead of comparing each items and selectively deleting and adding? The reloadData usually ends up in the following error.
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
In Short, I am looking for the following steps...
1)Fill the datasource array, show the data.
2)Fill the datasource array with new data, refresh the CollectionView to show the data.
Thanks in Advance
Try - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL finished))completion.
In your case, you want "an all new set of data", so to speak, so e.g:
[myCV performBatchUpdates:^{
// one of:
// a)
[myCV deleteSection:someIndexSetForTheEntireSection];
[myRealDataSource empty:someIndexSetForTheEntireSection];
//
// OR b)
[myCV deleteItemsAtIndexPaths:someSetOfIndexPaths];
[myRealDataSource removeIndexPaths:someSetOfIndexPaths];
// Either case:
NSArray *indexPaths = [myRealDataSource getNewDataAndReturnIndexPaths];
// if a)
[myCV insertSections:newIndexSetForNewSection];
// Either case:
[myCV insertItemsAtIndexPaths:newIndexSetForInsertions];
}
completion:^(BOOL finished) {
NSLog(#"Done.");
// Maybe signal something else if you want.
}];
performBatchUpdates:completion: will expect the deletions & insertions from the original data source check entering the function to add up to the correct data source size leaving the method. It will loudly complain otherwise.
If you only have one section (section 0), you can be much more general than specific index paths if you are always "removing everything" and "inserting a complete new set".
Another option to to use KVO to listen on insertions and removals from the data source and simply reloadData, reloadItemsAtIndexPaths: or reloadSections: as appropriate.
I prefer the reactive KVO version, as I tend to use collection views with Core Data, and not pseudo-static or in-memory NSArray's.
To figure out the CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION issue, I'd setup a breakpoint on all exceptions, and try to discover what is really triggering the issue. Likely your datasource is gone and there's a bad access when you try to read/write from it.
Suppose you arrive on your view then you can add data to your array in viewDidLoad method like so:
-(void)viewDidLoad
{
[super viewDidLoad];
// If you have already data
self.arr_sample=(NSArray *)array;
/*
When you want to download data from server than you have to call reloadData
method of collection because all delegate method already called before view load.
So loading data will take time to load data than you have to call all delegate
method of collectionview by calling reloadData method.
*/
[self loadData];
// Do any additional setup after loading the view from its nib.
}
but first of all you have set the delegate of collectionview .
do you want to download data from server than you can call reloaddata method of collection view. such as
-(void)loadData
{
// Do your downloading code her and call to reload data method of collectionview
[collectionview reloadData];
}
now again do you want to refill your array with new data on your button click than you can do
-(void)refillData
{
// Download new data code here and follow
[array removeAllObjects];
array=(NSArray *)newarray;
[collectionview reloadData];
}

add an object in my array each time I enter to the view

how can I add an object in my array each time I enter to my TableView
I put this code in viewDidLoad and viewDidAppear methods but it seems to doesn't work
:
- (void)viewDidLoad
{
[super viewDidLoad];
if (!myArray) {
myArray = [[NSMutableArray alloc] init];
}
[peopleListe insertObject:[NSDate date] atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
NSLog(#"%#",myArray);
}
when I put this code in a button it works
Thank you for your help
viewDidLoad is called once, when the view loads.
viewWillAppear is called every time you go into that view.
If you want to do something each time a view appears, put the code in viewWillAppear.
EDIT: It's possible that your array is getting dealloc'd. Try setting a breakpoint in dealloc as a simple way to see if that's the case:
- (void)dealloc {
NSLog(#"BYE!); // <-- put your breakpoint here
}
If it is, you'll have to (a) store your data somewhere else, or (b) keep this view/controller from being dealloced.
Also, who is your tableViewDelegate? That will have to implement methods returning the number of items in the table view and so on. I recommend having a read through the docs to get all those relationships sorted out.
You don't want to have that array as a property/ivar of your view controller. The view controller may, and will, get deallocated when it's not used (e.g. if it's inside the navigation controller, and you tap the "back" button to go to the previous screen.) When the view controller gets deallocated, your array obviously ceases to exist.
I suggest creating keeping that array in a separate place, e.g. in a singleton data object, or even (as a quick short-term solution) your app delegate.
About the code you posted: keep in mind that [UIViewController viewDidLoad] is only called once during the view controller's lifecycle. It may get called more than once, but that would mean that the original instance has been dealloc'd (and your original array is gone).

Memory management using ARC on iOS

Just have a quickly question (more of a curiosity thing) based on a problem I just solved (I will post the answer to my problem in the post, which can be found here: My former question
The thing is that I have this UITableView which contains custom cell objects. Every time you enter this view, I generate new cells for the UITableView like this:
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:#"UploadCellView" owner:self options:nil];
cell = customCell;
}
Which happens in the standard method:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Now the problem is that my custom cell objects listens for NSNotifications about upload objects happening in the background, so they can update its model data to their labels and progress bars etc. It happens like this (this is a method from the custom cell objects):
-(void) uploadProgress: (NSNotification*)notification
{
NSDictionary *userInfo = [notification userInfo];
NSNumber *uploadID = [userInfo valueForKey:#"uploadID"];
if (uploadID.integerValue == uploadActivity.uploadID)
{
UIProgressView *theProgressBar = (UIProgressView*)[self viewWithTag:progressBarTag];
[theProgressBar setProgress:(uploadActivity.percentageDone / 100) animated:YES];
UILabel *statusText = (UILabel*)[self viewWithTag:percentageTag];
[statusText setText:[NSString stringWithFormat:#"Uploader - %.f%% (%.01fMB ud af %.01fMB)", uploadActivity.percentageDone, uploadActivity.totalMBUploaded, uploadActivity.totalMBToUpload]];
}
}
When an upload finish they simply do this:
-(void) uploadFinished: (NSNotification*)notification
{
NSDictionary *userInfo = [notification userInfo];
NSNumber *uploadID = [userInfo valueForKey:#"uploadID"];
if (uploadID.integerValue == uploadActivity.uploadID)
{
[self setUploadComplete];
[[ApplicationActivities getSharedActivities] markUploadAsFinished:uploadActivity];
NSLog(#"BEGINNING RELOAD");
[parentTable reloadData];
NSLog(#"ENDING RELOAD");
}
}
Now the problem is when they call their owning tableview. When the view which the tableview is contained within dismisses, the old custom cell objects are still alive in the background getting NSNotfications. And when that upload is then done, the old custom cell objects from the former table views still tries to call that parentTable property which was set at that time, now resulting in calling random junk memory.
The way I solved this was to keep an array of all cell objects getting created in the table and then make them stop listening when the view is dismissed like this:
-(void) viewWillDisappear:(BOOL)animated
{
for (UploadCell *aCell in lol)
{
[aCell stopListening];
}
[self.navigationController popViewControllerAnimated:YES];
}
But this seems like a bit of a hack. How would I go about making sure that the custom cell objects are deleted when the view is dismissed? Because when the view is intialized again, new cells are simply made anyways, so I have no use for the old ones.
The custom view cells have a strong property pointer to the tableview they get associated with, but I thought the ARC would make sure that TableView pointer would not get invalidated then? Obviously it is somehow. Maybe because of the containing view being deleted when popped?
Sounds like the cells have a retain property pointing back to your UITableViewDataSource class.
They should instead have an assign property, then they will be released properly when the table view is released (which it currently cannot be if your cells are retaining it).
Also, the cells should shut down notifications when they are dropped out of the tableview, by overriding the cells didMoveToSuperview method:
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if ( [self superview] == nil )
{
[self unsubscribeFromYourNotifications];
}
}
That is so if they scroll off screen they will not be wasting resources updating things.
Have you considered a separate update model that keeps a map between uploadIDs and cells that listens for the notification? That way, the cells aren't responsible for updating the table themselves, the update model would do it. When the table goes away, you can shut down the update model.

Resources