Как реализовать UIScrollView с 1000+ subviews?

Я борюсь с написанием части приложения, которое должно вести себя как собственное приложение для iphone. Посмотрел книгу разработки приложений iphone sdk от Orielly, которая привела пример кода для реализации этого так называемого щелчка страницы. В коде сначала создавались все подпункты, а затем скрывались/отображались. В данный момент скрыты только 3 подсмотра. После долгих усилий я получил работу с приложением, которое в то время имело всего около 15 страниц.

Как только я добавил 300 страниц, стало ясно, что проблемы с производительностью/памятью связаны с таким подходом к распределению так много подзадач. Тогда я подумал, что для моего случая я должен просто выделить 3 подпункта и вместо того, чтобы скрывать/скрывать их. Может быть, я должен просто удалить/добавить subviews во время выполнения. Но не могу понять, может ли UIScrollView динамически обновлять содержимое. Например, в начале есть 3 кадра с разными x-смещениями (0, 320, 640) с экрана, как это понимается UIScrollView. Как только пользователь перейдет на третью страницу, как я могу убедиться, что я могу добавить 4-ю страницу и удалить 1-ю страницу, но UIScrollView не запутался?

Надеясь, что существует стандартное решение этой проблемы... может ли кто-нибудь руководствоваться?

Ответ 1

UIScrollView - это всего лишь подкласс UIView, поэтому он позволяет добавлять и удалять subviews во время выполнения. Предполагая, что у вас есть фотографии с фиксированной шириной (320 пикселей), и их 300, тогда ваш основной вид будет 300 * 320 пикселей в ширину. При создании представления прокрутки инициализируйте кадр шириной.

Таким образом, рамка просмотра прокрутки будет иметь размеры (от 0, 0) до (96000, 480). Всякий раз, когда вы добавляете subview, вам придется изменить его, чтобы он соответствовал правильной позиции в своем родительском представлении.

Итак, скажем, мы добавляем фотографию 4th в прокрутку. Он будет от (960, 480) до (1280, 480). Это легко вычислить, если вы можете каким-то образом связать индекс с каждым изображением. Затем используйте это для расчета рамки изображения, где индексы начинаются с 0:

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

к

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

Удаление первого изображения/подсмотра должно быть простым. Храните массив из трех подзонов, отображаемых в настоящее время на экране. Всякий раз, когда вы добавляете новое представление на экран, также добавляете его в конец этого массива, а затем удаляете также первое подвью в этом массиве с экрана.

Ответ 2

Следуя сказанному, вы можете показать тысячу элементов, используя только ограниченное количество ресурсов (и да, это немного мутный рисунок). Вот какой код, который может помочь вам сделать то, что вы хотите.

Класс UntitledViewController просто содержит UIScroll и устанавливает себя как его делегат. У нас есть NSArray с экземплярами NSString внутри как модель данных (в нем могут быть потенциально тысячи NSStrings), и мы хотим показать каждый из них в UILabel, используя горизонтальную прокрутку. Когда пользователь прокручивает, мы сдвигаем UILabels, чтобы поместить один слева, другой справа, чтобы все было готово для следующего события прокрутки.

Здесь интерфейс, довольно простой:

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

    NSArray *_objects;

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

@end

И вот реализация для этого класса:

@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

Надеюсь, это поможет!

Ответ 3

Большое спасибо Адриану за его очень простой и мощный образец кода. В этом коде была только одна проблема: когда пользователь сделал "двойную прокрутку" (I.E., когда он не дождался остановки анимации и повторной прокрутки прокрутки снова и снова).

В этом случае обновление позиции 3 подпроцессов действует только тогда, когда вызывается метод scrollViewDndEndDecelerating, а результат - задержка перед появлением подзонов на экране.

Этого можно легко избежать, добавив несколько строк кода:

в интерфейсе, просто добавьте это:

int refPage, currentPage;

в реализации, инициализировать refPage и currentPage в методе viewDidLoad следующим образом:

refpage = 0; 
curentPage = 0;

в реализации, просто добавьте метод scrollViewDidScroll, например:

- (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à!

Теперь подвыборы правильно заменены в правильных положениях, даже если пользователь никогда не останавливает анимацию и если метод scrollViewDidEndDecelerating никогда не вызывается!