In my UICollectionView with custom layout when i try to insert new cells, the UI become slow.
The Time and allocation instrument show me there is a probel with _computeGaps.
Each time i scroll, i need to insert new cells and the allocation instrument show me memory increase around +200Mb. It goes down when cells appear but it make my scroll very laggy, a couple of seconds first and it takes always more time on next scroll until the UI freeze completely.
Allocation instrument is full of this line:
__NSArrayI ...blabla UIKIT... __38-[UICollectionViewUpdate _computeGaps]_block_invoke171
It count in totla 1GB afer 2 minutes but only 24MB persistent. What is he doing ?
I know i refresh multiple section on the same time but i can't do it in small chunck otherwise i hit the 31 simulaneous animation limit on Ipad (more section to display).
Refresh code of my collection:
[UIView performWithoutAnimation:^{
[self.epgCollectionView performBatchUpdates:^{
if ([arrayIndexpathToInsert count] > 0) {
[self.epgCollectionView insertItemsAtIndexPaths:arrayIndexpathToInsert];
}
if ([arrayIndexpathToReload count] > 0) {
[self.epgCollectionView reloadItemsAtIndexPaths:arrayIndexpathToReload];
}
} completion:^(BOOL finished) {
}];
}];
I can paste more code but since i don't know where the problem is, i don't know which part i must show you.
Edit:
If i use:
[epgCollectionView reloadData];
There is no spike and the scrolling don't hang. Reloading the whole thing should take more time that insert/reload, no ? It's not the case for me.
Related
So, I am using the code below to cycle through json arrays if the json array returns positive, I add the json array to another array containing all of total arrays. This can take some time to complete however, because it is going through several different json urls plugging in different dynamic information.
I edited the code below to show more basic information. My problem is that data isnt actually entered into the array until the for loop is finished. I need it to be added as soon as it finds it, not after it runs through all the onlineChannels.count - Any help?
for (int i = 0; i<onlineChannels.count; i++) {
[jsonResults addObject:[parsingJson objectAtIndex:0]];
NSLog(#"%i",jsonResults.count);
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
[self.tableView reloadData];
} else {
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
[self.tableView reloadData];
}
}
My problem is that data isnt actually entered into the array until the for loop is finished.
How do you know?
I see that you're calling -reloadData, so perhaps you're expecting the change to be reflected in your table immediately? I believe that your objects are being added to the array immediately, but the change isn't displayed on the table right away. -reloadData doesn't actually redraw the table -- drawing happens as part of the main run loop. You're probably running this code on the main thread, and by doing so you're preventing the run loop from getting any time to redraw the table.
The best way to solve this sort of problem is to move time consuming processes like your loop to background threads. That will leave the main thread free to redraw the table as necessary, and it'll make your app more responsive as well.
Also, making the table reload all its data every time you add a single item even when that item's row isn't being displayed seems quite wasteful. Consider instead calling a method like reloadRowsAtIndexPaths:withRowAnimation: to update only the part of the table that has changed.
I'm trying to find a solution to a problem I just stumbled upon.
Tried to search for it but it's not that I'm looking for.
I'm doing a GPS-app with two tabbars. I track the distance in the map-view (using CLLocation) and when I change tab to a different view the stringtext that say what distance it is from when I started don't update immediately, it take a couple of seconds.
And when I press the stop button I want it to either wait those couple of seconds so the real distance updates. But I dont want to freeze the app.
(*NSDate future = [NSDate dateWithTimeIntervalSinceNow: 3.0 ];
[NSThread sleepUntilDate:future];)
I'm saving the string with the distance in a cell in the second tab, so if I multitask while the app is going I want to just start the app and press stop. And then the new distance will be correct and saved. Hope I didn't confuse you with this much text I hope you understand what I asking for!
thx
[label performSelector:#selector(setText:) withObject:newText afterDelay:3.0];
So what you want to do is use dispatch_after:
dispatch_after(3 seconds, dispatch_get_main_queue(), ^
{
myLabel.text = <the value you want to appear>;
} );
I'm trying to create a UITableView with dates, shouldn't be very exciting, I know. It starts around the current date, but the user should be able to scroll down (the future) and up (the past) as far as he/she wants. This results in a potentially infinite number of rows. So what's the way to go about creating this?
Returning NSIntegerMax as the number of rows already crashes the app, but even if it wouldn't, that still doesn't account for being able to scroll up. I could start half way of course, but eventually there's a maximum.
Any ideas how to do or fake this? Can I update/reload the table somehow without the user noticing, so I never run into a border?
SOLUTION:
I went with #ender's suggestion and made a table with a fixed amount of cells. But instead of reloading it when the user scrolls to near the edges of the fixed cells, I went with reloading the table when the scrolling grinds to a halt. To accomodate with a user scrolling great distances without stopping, I just increased the row count to 1000, putting the ROW_CENTER constant to 500. This is the method that takes care of updating the rows.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSArray *visible = [self.tableView indexPathsForVisibleRows];
NSIndexPath *upper = [visible objectAtIndex:0];
NSIndexPath *lower = [visible lastObject];
// adjust the table to compensate for the up- or downward scrolling
NSInteger upperDiff = ROW_CENTER - upper.row;
NSInteger lowerDiff = lower.row - ROW_CENTER;
// the greater difference marks the direction we need to compensate
NSInteger magnitude = (lowerDiff > upperDiff) ? lowerDiff : -upperDiff;
self.offset += magnitude;
CGFloat height = [self tableView:self.tableView heightForRowAtIndexPath:lower];
CGPoint current = self.tableView.contentOffset;
current.y -= magnitude * height;
[self.tableView setContentOffset:current animated:NO];
NSIndexPath *selection = [self.tableView indexPathForSelectedRow];
[self.tableView reloadData];
if (selection)
{
// reselect a prior selected cell after the reload.
selection = [NSIndexPath indexPathForRow:selection.row - magnitude inSection:selection.section];
[self.tableView selectRowAtIndexPath:selection animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
The magic breaks when a user scrolls to the edge of the table without stopping, but with the table view bounces property disabled, this merely feels like a minor glitch, yet totally acceptable. As always, thanks StackOverflow!
You should establish a fixed number of cells and adjust your datasource when the user scrolls near the end of the tableview. For example, you have an array with 51 dates (today, 25 future and 25 past). When the app tries to render a cell near one of the borders, reconfigure your array and call reloadData
You might also have a look at the "Advanced Scroll View Techniques" talk of the WWDC 2011. They showed how you would create a UIScrollView which scrolls indefinitely. It starts at about 5 mins. in.
Two thoughts:
Do you really need a UITableView? You could use a UIScrollView, three screens high. If end of scrolling is reached, layout your content and adjust scrolling position. This gives the illusion of infinite scrolling. Creating some date labels and arranging them in layoutSubviews: should not be too much of an effort.
If you really want to stick to UITableView you could think about having two UITableViews. If scrolling in your first one reaches a critical point, spin off a thread and populate the second one. Then at some point, exchange the views and trigger the scrolling manually so that the user does not note the change. This is just some idea from the top of my head. I have not implemented something like this yet, but I implemented the infinite UIScrollView.
I answered this question in another post: https://stackoverflow.com/a/15305937/2030823
Include:
https://github.com/samvermette/SVPullToRefresh
SVPullToRefresh handles the logic when UITableView reaches the bottom. A spinner is shown automatically and a callback block is fired. You add in your business logic to the callback block.
#import "UIScrollView+SVInfiniteScrolling.h"
// ...
[tableView addInfiniteScrollingWithActionHandler:^{
// append data to data source, insert new cells at the end of table view
// call [tableView.infiniteScrollingView stopAnimating] when done
}];
This question has already been asked: implementing a cyclic UITableView
I'm copying that answer here to make it easier because the asker hasn't ticked my answer.
UITableView is same as UIScrollView in scrollViewDidScroll method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualizing joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
Written and posted by Anup Kattel. Feel free to use this code. Please keep these comments if you don't mind.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit when data count is say for example less than 9.
The UIScrollView has a lot of information available to the programmer, but I dont see an obvious way to control the location that the control stop at after decelerating from a scroll gesture.
Basically I would like the scrollview to snap to specific regions of the screen. The user can still scroll like normal, but when they stop scrolling the view should snap to the most relevant location, and in the case of a flick gesture the deceleration should stop at these locations too.
Is there an easy way to do something like this, or should I consider the only way to accomplish this effect to write a custom scrolling control?
Since the UITableView is a UIScrollView subclass, you could implement the UIScrollViewDelegate method:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
And then compute what the closest desired target content offset is that you want, and set that on the inout CGPoint parameter.
I've just tried this and it works well.
First, retrieve the unguided offset like this:
CGFloat unguidedOffsetY = targetContentOffset->y;
Then Figure out through some math, where you'd want it to be, noting the height of the table header. Here's a sample in my code dealing with custom cells representing US States:
CGFloat guidedOffsetY;
if (unguidedOffsetY > kFirstStateTableViewOffsetHeight) {
int remainder = lroundf(unguidedOffsetY) % lroundf(kStateTableCell_Height_Unrotated);
log4Debug(#"Remainder: %d", remainder);
if (remainder < (kStateTableCell_Height_Unrotated/2)) {
guidedOffsetY = unguidedOffsetY - remainder;
}
else {
guidedOffsetY = unguidedOffsetY - remainder + kStateTableCell_Height_Unrotated;
}
}
else {
guidedOffsetY = 0;
}
targetContentOffset->y = guidedOffsetY;
The last line above, actually writes the value back into the inout parameter, which tells the scroll view that this is the y-offset you'd like it to snap to.
Finally, if you're dealing with a fetched results controller, and you want to know what just got snapped to, you can do something like this (in my example, the property "states" is the FRC for US States). I use that information to set a button title:
NSUInteger selectedStateIndexPosition = floorf((guidedOffsetY + kFirstStateTableViewOffsetHeight) / kStateTableCell_Height_Unrotated);
log4Debug(#"selectedStateIndexPosition: %d", selectedStateIndexPosition);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:selectedStateIndexPosition inSection:0];
CCState *selectedState = [self.states objectAtIndexPath:indexPath];
log4Debug(#"Selected State: %#", selectedState.name);
self.stateSelectionButton.titleLabel.text = selectedState.name;
OFF-TOPIC NOTE: As you probably guessed, the "log4Debug" statements are just logging. Incidentally, I'm using Lumberjack for that, but I prefer the command syntax from the old Log4Cocoa.
After the scrollViewDidEndDecelerating: and scrollViewDidEndDragging:willDecelerate: (the last one just when the will decelerate parameter is NO) you should set the contentOffset parameter of your UIScrollView to the desired position.
You also will know the current position by checking the contentOffset property of your scrollview, and then calculate the closest desired region that you have
Although you don't have to create your own scrolling control, you will have to manually scroll to the desired positions
To add to what felipe said, i've recently created a table view that snaps to cells in a similar way the UIPicker does.
A clever scrollview delegate is definitely the way to do this (and you can also do that on a uitableview, since it's just a subclass of uiscrollview).
I had this done by, once the the scroll view started decelerating (ie after scrollViewDidEndDragging:willDecelerate: is called), responding to scrollViewDidScroll: and computing the diff with the previous scroll event.
When the diff is less than say a 2 to 5 of pixels, i check for the nearest cell, then wait until that cell has been passed by a few pixels, then scroll back in the other direction with setContentOffset:animated:.
That creates a little bounce effect that is very nice for user experience, as it gives a good feedback on the snapping.
You'll have to be clever and not do anything when the table is bouncing at the top or bottom (comparing the offset to 0 or the content size will tell you that).
It works pretty well in my case because the cells are small (about 80-100px high), you might run into problems if you have a regular scroll view with bigger content areas.
Of course, you will not always decelerate past a cell, so in this case i just scroll to the nearest cell, and the animation looks jerky. Turns out with the right tuning, it barely ever happens, so i'm cool with this.
Spend a few hours tuning the actual values depending on your specific screen and you can get something decent.
I've also tried the naive approach, calling setContentOffset:animated: on scrollViewDidEndDecelerating: but it creates a really weird animation (or just plain confusing jump if you don't animate), that gets worse the lower the deceleration rate is (you'll be jumping from a slow movement to a much faster one).
So to answer the question:
- no, there is no easy way to do this, it'll take some time polishing the actual values of the previous algorithm, which might not work at all on your screen,
- don't try to create your own scroll view, you'll just waste time and badly reinvent a beautiful piece of engineering apple created with truck loads of bug. The scrollview delegate is the key to your problem.
Try something like this:
- (void) snapScroll;
{
int temp = (theScrollView.contentOffset.x+halfOfASubviewsWidth) / widthOfSubview;
theScrollView.contentOffset = CGPointMake(temp*widthOfSubview , 0);
}
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
if (!decelerate) {
[self snapScroll];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self snapScroll];
}
This takes advantage of the int's drop of the post-decimal digits. Also assumes all your views are lined up from 0,0 and only the contentOffset is what makes it show up in different areas.
Note: hook up the delegate and this works perfectly fine. You're getting a modified version - mine just has the actual constants lol. I renamed the variables so you can read it easy
My iPad app features a UITableView populated from a feed. Like most RSS readers, it displays a list of links to blog posts in reverse chronological order, with their titles and a summary of each post. The feed updates frequently, and is quite large, around 500 posts. I'm using libxml2 push parsing to efficiently download and parse the feed in an NSOperation subclass, constructing entry objects and updating a database as I go. But then I need to update the UITableView with changes.
So far, the app has been updating the UITableView for every post parsed, as it is parsed. The parser performs a selector on the main thread to do this work. But this leads to some serious lag for a couple of seconds if a lot of cells need to be updated. I can mitigate this by running the table update on a background thread, but it seems that this is not a good idea. So now I'm trying to figure out how to update the table more efficiently on the main thread.
I could just call reloadData when all the posts have been parsed, but it's not very user friendly: there's no animation to indicate that anything has changed, just a flash and the new data is there. I'd much rather have it animate to show that new posts are added and old posts removed. Existing posts that are not removed from the feed should be pushed down the table by the new posts appearing at the top.
I know this is possible. Byline, to give one example, does a beautiful job. Each post is added or removed from the UITableView one-at-a-time with no gaps showing the table background. All without making the UI in the least bit unresponsive. How is that done??
My latest attempt is to update the table only after all the posts have been parsed (the parser is quite fast, so it's not much of a delay). It then loads the existing posts in an NSDictionary mapping their IDs to their indexes in the array used as the table data source. It then iterates over every object in the newly-parsed array of posts, adding NSIndexPath for each to arrays that are later passed to -insertRowsAtIndexPaths:withRowAnimation:, -deleteRowsAtIndexPaths:withRowAnimation:, and -reloadRowsAtIndexPaths:withRowAnimation: as appropriate to insert, remove, move, or update cells. For 500 posts, this takes around 4 seconds to update, with the UI completely unresponsive. That time is used almost exclusively for the UITableView animated updates; iterating over the two arrays of posts takes very little time.
I then modified it so that those are updated without animation, and I have separate arrays to insert/delete/reload with animation only for row positions corresponding to currently-visible rows. This is better, but gaps appear as posts are removed and new ones added.
Sorry this is so long-winded, but here's the upshot:
How can I update a UITableView, with new cells pushed on, others pushed off, and still others moved from one position to another, with up to 500 cells in the UITableView (6-8 are visible at one time), and each animation happening in sequence, all while the UI remains completely responsive?
This question actually has three answers. That is, there are three parts to this question:
How to keep the UI responsive
How to keep the updating fast
How to make table update animation smooth
UI Responsiveness
To solve the first problem, I now make sure that no more than one table-updating message can be delivered on each iteration of the main event loop. That prevents the main thread from locking up if the background thread is feeding it stuff to do faster than it can cope with it.
This is done thanks to example code sent to me by Byline author Milo Bird, which I then integrated into Dave Dribin's DDInvocationGrabber. This interface makes it super easy to queue a method to be invoked on the next available iteration of the main event loop:
[[(id)delegate queueOnMainThread]
parserParsedEntries:parsedEntries
inPortal:parsedPortal];
I quite like how easy it is to use this method. The parser now uses it to call all of the delegate methods, most of which update the UI. I've released this code on GitHub.
Performance
As for performance, I was originally updating one UITableView row at a time. This was effective, but somewhat inefficient. I went back and studied the XMLPerformance example, where I noticed that the parser was waiting until it had collected 10 items before dispatching to the main thread to update the table. This was key to keeping the performance up without making the UI lock up by updating all 500 rows at once. I played around with updating 1, 10, and all 500 rows in a single call, and updating 10 seemed to offer the best tradeoff between performance and UI lockup. five would probably work pretty well, too.
Animation
And finally, there's the animation. Watching the “Mastering Table Views” WWDC 2010 session, I realized that my use of the deleteRowsAtIndexPaths:withRowAnimation: and updateRowsAtIndexPaths:withRowAnimation: methods was wrong. I had been keeping track of where things should be added and removed in the table and adjusting the indexes as appropriate, but it turns out that's not necessary. Inside a table update block, one only needs to reference the index of a row from before the update, regardless of how many may be inserted or deleted to change its position. The update block, apparently, does all that bookkeeping for you. (Go to about the 8:45 mark in the video for the key example).
Thus, the delegate method that updates the table for the number of entries passed to it by the parser (currently 10-at-a-time) now explicitly tracks the positions of rows to be updated or deleted from before the update block, like so:
NSMutableDictionary *oldIndexFor = [NSMutableDictionary dictionaryWithCapacity:posts.count];
int i = 0;
for (PostModel *e in posts) {
[oldIndexFor setObject:[NSNumber numberWithInt:i++] forKey:e.ident];
}
NSMutableArray *insertPaths = [NSMutableArray array];
NSMutableArray *deletePaths = [NSMutableArray array];
NSMutableArray *reloadPaths = [NSMutableArray array];
BOOL modified = NO;
for (PostModel *entry in entries) {
NSNumber *num = [oldIndexFor objectForKey:entry.ident];
NSIndexPath *path = [NSIndexPath indexPathForRow:currentPostIndex inSection:0];
if (num == nil) {
modified = YES;
[insertPaths addObject:path];
[posts insertObject:entry atIndex:currentPostIndex];
} else {
// Find its current position in the array.
NSUInteger foundAt = [posts indexOfObject:entry];
if (foundAt == currentPostIndex) {
// Reload it if it has changed.
if (entry.savedState != PostModelSavedStateUnmodified) {
modified = YES;
[posts replaceObjectAtIndex:foundAt withObject:entry];
[reloadPaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
}
} else {
// Move it.
modified = YES;
[posts removeObjectAtIndex:foundAt];
[posts insertObject:entry atIndex:currentPostIndex];
[insertPaths addObject:path];
[deletePaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
}
}
currentPostIndex++;
}
if (modified) {
[tableView beginUpdates];
[tableView insertRowsAtIndexPaths:insertPaths withRowAnimation:UITableViewRowAnimationTop];
[tableView deleteRowsAtIndexPaths:deletePaths withRowAnimation:UITableViewRowAnimationBottom];
[tableView reloadRowsAtIndexPaths:reloadPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
Comments welcome. It's entirely possible that there are more efficient ways to do this (the use of -[NSArray indexOfObject:] is particularly suspicious to me), and that I may have missed some other subtlety.
But even so, this is a huge improvement for my app. The UI now stays (mostly) responsive during a sync, the sync is fast, and the table update animation looks just about right.
Have you tried [tableView beginUpdates]; and [tableView endUpdate];?