Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement UIScrollView with 1000+ subviews?

I am struggling with writing portion of an app which should behave like the native iphone photo app. Looked at iphone sdk app development book from Orielly which gave an example code for implementing this so-called page-flicking. The code there first created all subviews and then hide/unhide them. At a given time only 3 subviews are visible rest are hidden. After much effort I got it working with app which at that time had only around 15 pages.

As soon as I added 300 pages, it became clear that there are performance/memory issues with that approach of pre-allocating so many subviews. Then I thought may be for my case I should just allocate 3 subviews and instead of hide/unhide them. May be I should just remove/add subviews at runtime. But can't figure out whether UIScrollView can dynamically update contents. For example, at the start there are 3 frames at different x-offsets ( 0, 320, 640 ) from the screen as understood by UIScrollView. Once user moves to 3rd page how do I make sure I am able to add 4th page and remove 1st page and yet UIScrollView doesn't get confused ?

Hoping there is a standard solution to this kind of problem...can someone guide ?

like image 875
climbon Avatar asked Jan 16 '10 22:01

climbon


3 Answers

Following what has been said, you can show thousand of elements using only a limited amount of resources (and yes, it's a bit of a Flyweight pattern indeed). Here's some code that might help you do what you want.

The UntitledViewController class just contains a UIScroll and sets itself as its delegate. We have an NSArray with NSString instances inside as data model (there could be potentially thousands of NSStrings in it), and we want to show each one in a UILabel, using horizontal scrolling. When the user scrolls, we shift the UILabels to put one on the left, another on the right, so that everything is ready for the next scroll event.

Here's the interface, rather straightforward:

@interface UntitledViewController : UIViewController <UIScrollViewDelegate>
{
@private
    UIScrollView *_scrollView;

    NSArray *_objects;

    UILabel *_detailLabel1;
    UILabel *_detailLabel2;
    UILabel *_detailLabel3;
}

@end

And here's the implementation for that class:

@interface UntitledViewController ()
- (void)replaceHiddenLabels;
- (void)displayLabelsAroundIndex:(NSInteger)index;
@end

@implementation UntitledViewController

- (void)dealloc 
{
    [_objects release];
    [_scrollView release];
    [_detailLabel1 release];
    [_detailLabel2 release];
    [_detailLabel3 release];
    [super dealloc];
}

- (void)viewDidLoad 
{
    [super viewDidLoad];

    _objects = [[NSArray alloc] initWithObjects:@"first", @"second", @"third", 
                @"fourth", @"fifth", @"sixth", @"seventh", @"eight", @"ninth", @"tenth", nil];

    _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 460.0)];
    _scrollView.contentSize = CGSizeMake(320.0 * [_objects count], 460.0);
    _scrollView.showsVerticalScrollIndicator = NO;
    _scrollView.showsHorizontalScrollIndicator = YES;
    _scrollView.alwaysBounceHorizontal = YES;
    _scrollView.alwaysBounceVertical = NO;
    _scrollView.pagingEnabled = YES;
    _scrollView.delegate = self;

    _detailLabel1 = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 460.0)];
    _detailLabel1.textAlignment = UITextAlignmentCenter;
    _detailLabel1.font = [UIFont boldSystemFontOfSize:30.0];
    _detailLabel2 = [[UILabel alloc] initWithFrame:CGRectMake(320.0, 0.0, 320.0, 460.0)];
    _detailLabel2.textAlignment = UITextAlignmentCenter;
    _detailLabel2.font = [UIFont boldSystemFontOfSize:30.0];
    _detailLabel3 = [[UILabel alloc] initWithFrame:CGRectMake(640.0, 0.0, 320.0, 460.0)];
    _detailLabel3.textAlignment = UITextAlignmentCenter;
    _detailLabel3.font = [UIFont boldSystemFontOfSize:30.0];

    // We are going to show all the contents of the _objects array
    // using only these three UILabel instances, making them jump 
    // right and left, replacing them as required:
    [_scrollView addSubview:_detailLabel1];
    [_scrollView addSubview:_detailLabel2];
    [_scrollView addSubview:_detailLabel3];

    [self.view addSubview:_scrollView];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [_scrollView flashScrollIndicators];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self displayLabelsAroundIndex:0];
}

- (void)didReceiveMemoryWarning 
{
    // Here you could release the data source, but make sure
    // you rebuild it in a lazy-loading way as soon as you need it again...
    [super didReceiveMemoryWarning];
}

#pragma mark -
#pragma mark UIScrollViewDelegate methods

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    // Do some initialization here, before the scroll view starts moving!
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self replaceHiddenLabels];
}

- (void)displayLabelsAroundIndex:(NSInteger)index
{
    NSInteger count = [_objects count];
    if (index >= 0 && index < count)
    {
        NSString *text = [_objects objectAtIndex:index];
        _detailLabel1.frame = CGRectMake(320.0 * index, 0.0, 320.0, 460.0);
        _detailLabel1.text = text;
        [_scrollView scrollRectToVisible:CGRectMake(320.0 * index, 0.0, 320.0, 460.0) animated:NO];

        if (index < (count - 1))
        {
            text = [_objects objectAtIndex:(index + 1)];
            _detailLabel2.frame = CGRectMake(320.0 * (index + 1), 0.0, 320.0, 460.0);
            _detailLabel2.text = text;
        }

        if (index > 0)
        {
            text = [_objects objectAtIndex:(index - 1)];
            _detailLabel3.frame = CGRectMake(320.0 * (index - 1), 0.0, 320.0, 460.0);
            _detailLabel3.text = text;
        }
    }
}

- (void)replaceHiddenLabels
{
    static const double pageWidth = 320.0;
    NSInteger currentIndex = ((_scrollView.contentOffset.x - pageWidth) / pageWidth) + 1;

    UILabel *currentLabel = nil;
    UILabel *previousLabel = nil;
    UILabel *nextLabel = nil;

    if (CGRectContainsPoint(_detailLabel1.frame, _scrollView.contentOffset))
    {
        currentLabel = _detailLabel1;
        previousLabel = _detailLabel2;
        nextLabel = _detailLabel3;
    }
    else if (CGRectContainsPoint(_detailLabel2.frame, _scrollView.contentOffset))
    {
        currentLabel = _detailLabel2;
        previousLabel = _detailLabel1;
        nextLabel = _detailLabel3;
    }
    else
    {
        currentLabel = _detailLabel3;
        previousLabel = _detailLabel1;
        nextLabel = _detailLabel2;
    }

    currentLabel.frame = CGRectMake(320.0 * currentIndex, 0.0, 320.0, 460.0);
    currentLabel.text = [_objects objectAtIndex:currentIndex];

    // Now move the other ones around
    // and set them ready for the next scroll
    if (currentIndex < [_objects count] - 1)
    {
        nextLabel.frame = CGRectMake(320.0 * (currentIndex + 1), 0.0, 320.0, 460.0);
        nextLabel.text = [_objects objectAtIndex:(currentIndex + 1)];
    }

    if (currentIndex >= 1)
    {
        previousLabel.frame = CGRectMake(320.0 * (currentIndex - 1), 0.0, 320.0, 460.0);
        previousLabel.text = [_objects objectAtIndex:(currentIndex - 1)];
    }
}

@end

Hope this helps!

like image 87
Adrian Kosmaczewski Avatar answered Oct 04 '22 10:10

Adrian Kosmaczewski


UIScrollView is just a subclass of UIView so it's possible to add and remove subviews at runtime. Assuming you have fixed width photos (320px) and there are 300 of them, then your main view would be 300 * 320 pixels wide. When creating the scroll view, initialize the frame to be that wide.

So the scroll view's frame would have the dimensions (0, 0) to (96000, 480). Whenever you are adding a subview, you will have to change it's frame so it fits in the correct position in its parent view.

So let's say, we are adding the 4th photo to the scroll view. It's frame would be from (960, 480) to (1280, 480). That is easily to calculate, if you can somehow associate an index with each picture. Then use this to calculate the picture's frame where indexes start at 0:

Top-Left -- (320 * (index - 1), 0)

to

Bottom-Right -- (320 * index, 480)

Removing the first picture/subview should be easy. Keep an array of the 3 subviews currently on-screen. Whenever you are adding a new subview to the screen, also add it to the end of this array, and then remove the first subview in this array from the screen too.

like image 37
Anurag Avatar answered Oct 04 '22 10:10

Anurag


Many thanks to Adrian for his very simple and powerfull code sample. There was just one issue with this code : when the user made a "double scroll" (I.E. when he did not wait for the animation to stop and rescroll the scrollview again and again).

In this case, the refresh for the position of the 3 subviews is effective only when the "scrollViewDidEndDecelerating" method is invoked, and the result is a delay before the apparition of the subviews on the screen.

This can be easily avoided by adding few lines of code :

in the interface, just add this :

int refPage, currentPage;

in the implementation, initialize refPage and currentPage in the "viewDidLoad" method like this :

refpage = 0; 
curentPage = 0;

in the implementation, just add the "scrollViewDidScroll" method, like this :

- (void)scrollViewDidScroll:(UIScrollView *)sender{
    int currentPosition = floor(_scrollView.contentOffset.x);
    currentPage = MAX(0,floor(currentPosition / 340)); 
//340 is the width of the scrollview...
    if(currentPage != refPage)  { 
        refPage = currentPage;
        [self replaceHiddenLabels];
        }
    }

et voilà !

Now, the subviews are correctly replaced in the correct positions, even if the user never stop the animation and if the "scrollViewDidEndDecelerating" method is never invoked !

like image 20
Chrysotribax Avatar answered Oct 04 '22 11:10

Chrysotribax