am trying to place a ScrollView in my app that has 1000,000 record, this scrollView will load when the app launches, so the app is not running until the million 1000 000 record which takes a lot of time, i was wondering is there any way to show the app and the scrollView while records are loading (show the scrollView while adding its records), below the code am using:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self loadIt];
}
- (void)loadIt{
float startX = 0;
float startY = 0;
[_bigScroll setContentSize:CGSizeMake(320, 312500)];
_bigScroll.pagingEnabled = NO;
for (counter=0; counter<999999; counter++)
{
UIButton *tester=[[UIButton alloc] initWithFrame:CGRectMake(startX, startY, 10, 10)];
if (counter % 2 == 0) {
[tester setBackgroundColor:[UIColor whiteColor]];
}
else
{
[tester setBackgroundColor:[UIColor grayColor]];
}
[_bigScroll addSubview:tester];
[tester release];
if (startX == 320) {
startX = 0;
startY += 10;
}
else
startX += 10;
NSLog(#"counter = %d", counter);
}
}
Please advice.
Is there any way to show the app and the scrollView while records are loading ?
Try to use [self performSelector:#selector(loadIt) withObject:nil]; or
[self performSelector:#selector(loadIt) withObject:nil afterDelay:0.2];
It will not block your UI until the execution of this method.
You are loading lots of records. Actually you should not load all records at at time. You should use mechanism something like tableview is using i.e.load only those record which are in visible area of scrollview. Don't load new rows until the scroll and you should reuse row or views so speedup the scrolling.
Apple's documentation for UIScrollView is very clear that the scrolled view should be tiled, with your application providing tiles as the view scrolls.
The object that manages the drawing of content displayed in a scroll view should tile the content’s subviews so that no view exceeds the size of the screen. As users scroll in the scroll view, this object should add and remove subviews as necessary.
This is necessary both for performance and memory usage: the scrollable view is backed by a CALayer, which in turn is backed by a bitmap. The same is true for each of the UIButton objects created.
Whilst it is not surprising that this takes a long time, it's more of a mystery that your app hasn't been terminated for using too much memory.
Both UITableView and UICollectionView are examples of views that tile their content. You may find you can use one of these to implement you requirements, and if not, follow the model they use.
You don't need to create 1000,000 views . You can create views dynamically and remove the previous views those are not visible at the screen space. So at the time of scrolling you can create new views and remove the views those are out of visible area of screen.
This will help you to save memory otherwise whether you are using ARC in your project if you load that much number of views in memory there will surely a chance of crash , ARC will not help you in that case.
once try this Change the code in the
-viewdidload()
{
[self loadIt];//change this to
[self performSelectorInBackground:#selector(loadIt) withObject:nil];
}
Related
My app for the iPhone Contains 170 images. I read them into an array, and in the loop add a picture in the viewfinder, then put the image View as sub view of scroll view.
When running my app uses too much memory 700mb. I tried to reduce the size of the pictures, but it didn't work.
One of my friends said that I should add only images # 1 and # 2. When the user block is scrolled to the picture No. 1, then only show the picture No. 2. Then the picture No. 1 to remove from the image viewer and add the picture No. 3.
He says that in this way I can maintain the normal memory consumption. But I don't understand how to do this?
Could you help with an example? Thanks in advance.
Not using UICollectionView
Here is my code:
- (void)addImageViewToScrollView {
_assetsArray = [[NSMutableArray alloc] init];
for (int j = 0; j < 170; j++) {
[_assetsArray addObject:[UIImage imageNamed:[NSString stringWithFormat:#"%d",j]]];
}
//scrollView add subview ImageView
for (int i = 0; i < [_assetsArray count]; i++) {
CGRect frame;
frame.origin.x = self.scrollView.frame.size.width *i;
frame.origin.y = 0;
frame.size = self.scrollView.frame.size;
_imageView = [[UIImageView alloc]init];
_imageView.image = [_assetsArray objectAtIndex:i];
_imageView.frame = frame;
[self.scrollView addSubview:_imageView];
}
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * self.assetsArray.count, self.scrollView.frame.size.height);
}
--
Use a UICollectionViewController to solve this issue.
This way only the cells that are required on screen are loaded and all others images are can be popped from cache when you hit a memory warning.
Collection Views work very similar to table views.
To get the collection view to suit what you're doing you'll need to set the flow layout to horizontal scrolling and set the cell sizes to your view's height and width.
Seems like you are trying to add170images onto UIScrollView using UIImageView. Why not use dedicated UITableView's or UICollectionView's? Object of Reusable Cell is to maintain run-time memory consumption. Let's recap on this,
You have 170 ImageViews (_assetsArray) ; expensive consumption
You have 170 Images
Versus, Using UITableView or UICollectionView
You have 1 ImageView in a Cell with 170 rows
You have DataSource of 170 Images were loaded
Your cell will be re-cycled when it needed to display correspond to DataSource
Also, for supporting the multitude images, there are good libraries to reduce images cache. Try to use SDWebImage or FastImageCache. I personally like FIC but you might need to understand the concept.
I'm trying to increase the scrolling performance of my UIScrollView. I have a lot of UIButtons on it (they could be hundreds): every button has a png image set as background.
If I try to load the entire scroll when it appears, it takes too much time. Searching on the web, I've found a way to optimize it (loading and unloading pages while scrolling), but there's a little pause in scrolling everytime I have to load a new page.
Do you have any advice to make it scroll smoothly?
Below you can find my code.
- (void)scrollViewDidScroll:(UIScrollView *)tmpScrollView {
CGPoint offset = tmpScrollView.contentOffset;
//322 is the height of 2*2 buttons (a page for me)
int currentPage=(int)(offset.y / 322.0f);
if(lastContentOffset>offset.y){
pageToRemove = currentPage+3;
pageToAdd = currentPage-3;
}
else{
pageToRemove = currentPage-3;
pageToAdd = currentPage+3;
}
//remove the buttons outside the range of the visible pages
if(pageToRemove>=0 && pageToRemove<=numberOfPages && currentPage<=numberOfPages){
for (UIView *view in scrollView.subviews)
{
if ([view isKindOfClass:[UIButton class]]){
if(lastContentOffset<offset.y && view.frame.origin.y<pageToRemove*322){
[view removeFromSuperview];
}
else if(lastContentOffset>offset.y && view.frame.origin.y>pageToRemove*322){
[view removeFromSuperview];
}
}
}
}
if(((lastContentOffset<offset.y && lastPageToAdd+1==pageToAdd) || (lastContentOffset>offset.y && lastPageToAdd-1==pageToAdd)) && pageToAdd>=0 && pageToAdd<=numberOfPages){
int tmpPage=0;
if((lastContentOffset<offset.y && lastPageToAdd+1==pageToAdd)){
tmpPage=pageToAdd-1;
}
else{
tmpPage=pageToAdd;
}
//the images are inside the application folder
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
for(int i=0;i<4;i++){
UIButton* addButton=[[UIButton alloc] init];
addButton.layer.cornerRadius=10.0;
if(i + (tmpPage*4)<[imagesCatalogList count]){
UIImage* image=[UIImage imageWithContentsOfFile:[NSString stringWithFormat: #"%#/%#",docDir,[imagesCatalogList objectAtIndex:i + (tmpPage*4)]]];
if(image.size.width>image.size.height){
image=[image scaleToSize:CGSizeMake(image.size.width/(image.size.height/200), 200.0)];
CGImageRef ref = CGImageCreateWithImageInRect(image.CGImage, CGRectMake((image.size.width-159.5)/2,(image.size.height-159.5)/2, 159.5, 159.5));
image = [UIImage imageWithCGImage:ref];
}
else if(image.size.width<image.size.height){
image=[image scaleToSize:CGSizeMake(200.0, image.size.height/(image.size.width/200))];
CGImageRef ref = CGImageCreateWithImageInRect(image.CGImage, CGRectMake((image.size.width-159.5)/2, (image.size.height-159.5)/2, 159.5, 159.5));
image = [UIImage imageWithCGImage:ref];
}
else{
image=[image scaleToSize:CGSizeMake(159.5, 159.5)];
}
[addButton setBackgroundImage:image forState:UIControlStateNormal];
image=nil;
addButton.frame=CGRectMake(width, height, 159.5, 159.5);
NSLog(#"width %i height %i", width, height);
addButton.tag=i + (tmpPage*4);
[addButton addTarget:self action:#selector(modifyImage:) forControlEvents:UIControlEventTouchUpInside];
[tmpScrollView addSubview:addButton];
addButton=nil;
photos++;
}
}
}
lastPageToAdd=pageToAdd;
lastContentOffset=offset.y;
}
Here's a few recommendations:
1) First, understand that scrollViewDidScroll: will get called continuously, as the user scrolls. Not just once per page. So, I would make sure that you have logic that ensures that the real work involved in your loading is only triggered once per page.
Typically, I will keep a class ivar like int lastPage. Then, as scrollViewDidScroll: is called, I calculate the new current page. Only if it differs from the ivar do I trigger loading. Of course, then you need to save the dynamically calculated index (currentPage in your code) in your ivar.
2) The other thing is that I try not to do all the intensive work in the scrollViewDidScroll: method. I only trigger it there.
So, for example, if you take most of the code you posted and put it in a method called loadAndReleasePages, then you could do this in the scrollViewDidScroll: method, which defers the execution until after scrollViewDidScroll: finishes:
- (void)scrollViewDidScroll:(UIScrollView *)tmpScrollView {
CGPoint offset = tmpScrollView.contentOffset;
//322 is the height of 2*2 buttons (a page for me)
int currentPage = (int)(offset.y / 322.0f);
if (currentPage != lastPage) {
lastPage = currentPage;
// we've changed pages, so load and release new content ...
// defer execution to keep scrolling responsive
[self performSelector: #selector(loadAndReleasePages) withObject: nil afterDelay:0];
}
}
This is code that I've used since early iOS versions, so you can certainly replace the performSelector: call with an asynchronous GCD method call, too. The point is not to do it inside the scroll view delegate callback.
3) Finally, you might want to experiment with slightly different algorithms for calculating when the scroll view has actually scrolled far enough that you want to load and release content. You currently use:
int currentPage=(int)(offset.y / 322.0f);
which will yield integer page numbers based on the way the / operator, and the float to int cast works. That may be fine. However, you might find that you want a slightly different algorithm, to trigger the loading at a slightly different point. For example, you might want to trigger the content load as the page has scrolled exactly 50% from one page to the next. Or you might want to trigger it only when you're almost completely off the first page (maybe 90%).
I believe that one scrolling intensive app I wrote actually did require me to tune the precise moment in the page scroll when I did the heavy resource loading. So, I used a slightly different rounding function to determine when the current page has changed.
You might play around with that, too.
Edit: after looking at your code a little more, I also see that the work you're doing is loading and scaling images. This is actually also a candidate for a background thread. You can load the UIImage from the filesystem, and do your scaling, on the background thread, and use GCD to finally set the button's background image (to the loaded image) and change its frame back on the UI thread.
UIImage is safe to use in background threads since iOS 4.0.
Don't touch a line of code until you've profiled. Xcode includes excellent tools for exactly this purpose.
First, in Xcode, make sure you are building to a real device, not the simulator
In Xcode, choose Profile from the Product menu
Once Instruments opens, choose the Core Animation instrument
In your app, scroll around in the scroll view you're looking to profile
You'll see the real time FPS at the top, and in the bottom, you'll see a breakdown of all function and method calls based on total time ran. Start drilling down the highest times until you hit methods in your own code. Hit Command + E to see the panel on the right, which will show you full stack traces for each function and method call you click on.
Now all you have to do is eliminate or optimize the calls to the most "expensive" functions and methods and verify your higher FPS.
That way you don't waste time optimizing blind, and potentially making changes that have no real effect on the performance.
My answer is really a more general approach to improving scroll view and table view performance. To address some of your particular concerns, I highly recommend watching this WWDC video on advanced scroll view use: https://developer.apple.com/videos/wwdc/2011/includes/advanced-scrollview-techniques.html#advanced-scrollview-techniques
The line that is likely killing your performance is:
addButton.layer.cornerRadius=10.0;
Why? Turns out the performance for cornerRadius is AWFUL! Take it out... guaranteed huge speedup.
Edit: This answer sums up what you should do quite clearly.
https://stackoverflow.com/a/6254531/537213
My most common solution is to rasterize the Views:
_backgroundView.layer.shouldRasterize = YES;
_backgroundView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
But it works not in every situation.. Just try it
Trying to get around a crash that is happening on some iOS devices, in conjunction with advice from Apple to "not cause allocation spikes". How can I change this code to not happen all at once?
for (Item *item in self.items) {
ItemView *itemView = [[ItemView alloc] initWithFrame:CGRectMake(xPos, kYItemOffsetIphone, kItemWidthIphone, kItemHeightIphone) ];
itemView.delegate = self;
[itemView layoutWithData:item]; //this just adds an imageView and button
[self.scrollView addSubview:itemView];
xPos += kXItemSpacingIphone;
}
There are around 20 objects in the self.items array, which are used to build the 20 ItemViews. Again, is there some way to make this code less "allocation intensive"?
I personally do something along the lines of:
Make my view controller the delegate of the scroll view (if you do this in code, you have to modify your view controller's .h to say that it conforms to UIScrollViewDelegate).
Define a scrollViewDidScroll method that (a) determines the frame of the visible portion of the scroll view; (b) determine which of the subviews intersect with that visible portion; (c) load the items that are visible, and unload the ones that aren't.
So, for example, it might look something like the following:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Determine the frame of the visible portion of the scrollview.
CGRect visibleScrollViewFrame = scrollView.bounds;
visibleScrollViewFrame.origin = scrollView.contentOffset;
// Now iterate through the various items, remove the ones that are not visible,
// and show the ones that are.
for (Item *itemObject in self.itemCollection)
{
// Determine the frame within the scrollview that the object does (or
// should) occupy.
CGRect itemObjectFrame = [self getItemObjectFrame:itemObject];
// see if those two frames intersect
if (CGRectIntersectsRect(visibleScrollViewFrame, itemObjectFrame))
{
// If it's visible, then load it (if it's not already).
// Personally, I have my object have a boolean property that
// tells me whether it's loaded or not. You can do this any
// way you want.
if (!itemObject.loaded)
[itemObject loadItem];
}
else
{
// If not, go ahead and unload it (if it's loaded) to conserve memory.
if (itemObject.loaded)
[itemObject unloadItem];
}
}
}
That's the basic idea. You can certainly optimize this logic based upon your app's particular design, but this is how I generally do it.
The iPad app I'm working on is a book. To jump to a specific page, the user can press a button that overlays a view top of the current view, displaying images of thumbnails of each page in the book.
When the user goes through the book sequentially and displays this thumbnails menu, the scrolling animation is smooth and fine if the user showed the menu . The problem happens if the user calls showBookmarkMenu after having loaded about fifteen pages, the scrollview animation is very very slow, and the scrollview doesn't catch touches anymore.
I noticed that scrollViewDidEndDecelerating gets called when the scrolling animation is normal and smooth (shortly after loading the app), but it doesn't get called after the user has gone through several pages. So one hypothesis is that the CPU is struggling with the animation of the positioning of the scrollview's content. I ran the app using Instruments' Activity Monitor, but there are times when the app uses 97% and more of the CPU and the scrollview scrolls fine...
Any thoughts on this issue? I've posted my code below.
MainClass.m
//Called when user presses the open/close bookmark menu button
-(IBAction)toggleBookmarksMenu{
if([bookMarkMenu isHidden]){
[currentPage.view addSubview:bookMarkMenu];
[bookMarkMenu showBookmarkMenu];
}
else{
[bookMarkMenu hideBookmarksMenu];
}
}
ScrollViewClass.h
#interface BookmarkManager : UIView<UIScrollViewDelegate>{
UIScrollView *thumbnailScrollView;
}
#property (strong, nonatomic) UIScrollView *thumbnailScrollView;
#property (strong) id <BookmarkManagerDelegate> bookmarkManagerDelegate;
-(void)showBookmarkMenu;
-(void)hideBookmarksMenu;
#end
ScrollViewClass.m
-(void)showBookmarkMenu{
[self setHidden:NO];
[UIView animateWithDuration:0.5
animations:^{
self.center = CGPointMake(512, 384);
}
];
}
-(void)hideBookmarksMenu{
[UIView animateWithDuration:1
animations:^{
self.center = CGPointMake(512, -120);
}
completion:^(BOOL finished){
[self setHidden:YES];
[self removeFromSuperview];
}
];
}
-(id)init{
self = [super initWithFrame:CGRectMake(0, 0, 1024, 768)];
if(self){
[self setBackgroundColor:[UIColor clearColor]];
self.center = CGPointMake(512, 0);
thumbnailScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 1024, 120)];
[thumbnailScrollView setBackgroundColor:[UIColor clearColor]];
thumbnailScrollView.showsHorizontalScrollIndicator = NO;
//Add the UIButtons with images of the thumbnails
for(int i = 0; i < totalPages; i++){
UIButton *pageThumbnail = [UIButton buttonWithType:UIButtonTypeCustom];
pageThumbnail.frame = CGRectMake(0, 0, 125, 95);
[pageThumbnail setBackgroundImage:[UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/p%d_thumb.png", [[NSBundle mainBundle] resourcePath], i]] forState:UIControlStateNormal];
[thumbnailScrollView addSubview:pageThumbnail];
[pageThumbnail addTarget:self action:#selector(thumbnailTapped:) forControlEvents:UIControlEventTouchDown];
}
[self addSubview:thumbnailScrollView];
[thumbnailScrollView setContentSize:CGSizeMake(totalPages * 125 + (20*(totalPages+1)), 120)];
[thumbnailScrollView setDelegate:self];
[self setHidden:YES];
}
return self;
}
I have to go with possible low memory issue.
A possible alternative to using a slew of buttons is using UITableView. The way your code is currently working, it loads up ALL the buttons with images. For a large book this could be painful.
Using UITableView you only use as much memory as you see (about). And, since each image is loaded dynamically, your memory usage is only as much as is displayed. That would be how I would go about it (actually, I'm doing that now, just not with a book).
A shot in the dark, based on your observation that the scrolling becomes slow after loading 15 pages or so: possibly your device is busy handling a low memory condition. In such cases, as you possibly know, a system wide notification is sent to a considerable number of apps/objects for them to recover as much memory as possible.
Could you check if at more or less the same time when the scrolling becomes slow your app is executing didReceiveMemoryWarning?
If you confirm that the issue could be related to memory saturation/reclaiming, then I would suggest implementing a lazy loading scheme for your images:
you only load images when you are required to display them;
you only keep in memory 3-5 images total, to ensure a smooth scrolling.
The basic step requires id providing your delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
implementation. Here you will preload images:
knowing your position, you know your current image (say, image number N);
unload images N-2, N+2;
load images N-1, N+1.
The images to load/unload I provided are fine if you just want one "buffer" image.
In any case, if you google "iso scroll view lazy loading" you will find plenty of info.
Turns out it wasn't a low memory issue, but an overly busy CPU issue.
It is the CPU that does the calculations required for the scrollview's scrolling animations, and when the scrolling becomes this slow I thought I'd try to figure out why I was using 97% of the CPU in the first place. Turns out that past page 15, I had CPU-intensive recursive functions (calculating UIBezierPaths for another part of the app) caught in an infinite loop. The app was calculating hundreds of UIBezierPaths a second, and there reached a point where the CPU just couldn't keep up with the calculations for the scrollview's animation.
Once I made sure the recursive functions stopped calling themselves when they were not needed, CPU usage remained under 20% throughout the app, and the scrollview performed perfectly well.
I would like to implement an app using a UIScrollView with paging, similar to the apple weather app.
But I am a little concerned about performance. The example implementation I have been using loads all of the views then the application launches. After a certain point, once this prove slow?
I wonder how Apple's camera roll is dealing with this, where a user may have 100+ photos that can be scrolled through. Should I try to figure out a way to build the view only when it is needed? Or maybe there is a way to replicate the dequeue reusable cell technique from a UITableView, only for horizontal view loading, since each view will have the same layout.
By far the most efficient solution (and this is used in many photo-browsing apps such as Facebook, and probably the native Photos app too) is going to be to load the content on-demand, just as UITableView does. Apple's StreetScroller sample project should get you on the right track.
A very efficient solution, is to make sure to reuse any views whenever possible. If you are going to be simply displaying images, you could use a subclass of UIScrollView, and layout these reusable views within layoutSubviews. Here you could detect what views are visible and not visible and create the subviews as needed.
An example dequeuing function may look like:
- (UIImageView *)dequeueReusableTileWithFrame:(CGRect) frame andImage:(UIImage *) image
{
UIImageView *tile = [reusableTiles anyObject];
if (tile) {
[reusableTiles removeObject:tile];
tile.frame = frame;
}
else {
tile = [[UIImageView alloc] initWithFrame:frame];
}
tile.image = image;
return tile;
}
Where reusableTiles is just an iVar of NSMutableSet type. You could then use this to load fetch any currently offscreen image views and quickly and easily bring them back into view.
Your layoutSubviews may look something like:
- (void)layoutSubviews {
[super layoutSubviews];
CGRect visibleBounds = [self bounds];
CGPoint contentArea = [self contentOffset];
//recycle all tiles that are not visible
for (GSVLineTileView *tile in [self subviews]) {
if (! CGRectIntersectsRect([tile frame], visibleBounds)) {
[reusableTiles addObject:tile];
[tile removeFromSuperview];
}
}
int col = firstVisibleColumn = floorf(CGRectGetMinX(visibleBounds)/tileSize.width);
lastVisibleColumn = floorf(CGRectGetMaxX(visibleBounds)/tileSize.width) ;
int row = firstVisibleRow = floorf(CGRectGetMinY(visibleBounds)/tileSize.height);
lastVisibleRow = floorf(CGRectGetMaxY(visibleBounds)/tileSize.height);
while(row <= lastVisibleRow)
{
col = firstVisibleColumn;
while (col <= lastVisibleColumn)
{
if(row < firstDisplayedRow || row > lastDisplayedRow || col < firstDisplayedColumn || col >lastDisplayedColumn)
{
UImageView* tile = [self dequeueReusableTileWithFrame:CGRectMake(tileSize.width*col, tileSize.height*row, tileSize.width, tileSize.height) andImage:YourImage];
[self addSubview:tile];
}
++col;
}
++row;
}
firstDisplayedColumn = firstVisibleColumn;
lastDisplayedColumn = lastVisibleColumn;
firstDisplayedRow = firstVisibleRow;
lastDisplayedRow = lastVisibleRow;
}
I used something similar to this to tile in areas of a line when I was working with an exceptionally large area of a scroll view and it seemed to work quite well. Sorry for any typos that I may have created when updating this for an image view instead of my custom tileView class.