Вы еще его увидите

Вы еще его увидите

В последнее время все чаще начал замечать ошибку у iPhone-разработчиков (неопытных и даже у тех, кто считает себя опытными) связанную с использованием ячеек в UITableView. Разработчики не понимают, что в UITableView ячейки не создаются каждый раз, а используются уже созданные!

Начнем по-порядку. Всем нам известен метод tableView:cellForRowAtIndexPath:. Привожу наиболее распространенный код (цифрами отмечены специальные места, о которых речь пойдет ниже):

-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

// 1
static NSString* cellID = @"cellID";
UITableViewCell* cell;
cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if ( !cell ) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID] autorelease];
// 2
}
// 3
return cell;
}

Мы прекрасно знаем что должен возвращать этот метод. Он должен возвращать ячейку, которая вот-вот появиться на экране.

Рассмотрим специально обозначенные места:

1. Подготовительная часть. Тут мы выясняем создана ли уже ячейка конкретного типа (cellID).

2. Если ячейка не создана — в этом месте самое время ее создать. Вызывается столько раз, сколько ячеек первоначально поместится на экране, в примере ниже — 12 раз. В последствии эти 12 ячеек будут переиспользованны.

3. В этом месте происходит наделение свойств ячейки конкретными значениями (например тайтлом или картинкой).

В каком же месте делают ошибку разработчики? В местах (2) и (3).

Для более детального описания ошибки приведу пример. Предположим, что нам нужно показать 20 строк таблицы, в некоторых из них должна быть кнопка. Наличие кнопки определяется каким-либо критерием. Создайте новый UIViewController, добавьте в него UITableView через Interface Builder, назначьте UITableViewDataSource и UITableViewDelegate на UIViewController. Будем генерировать массив из 20 словарей. В некоторых из них случайным образом определим флаг, который будет говорить нам о том, показывать ли кнопку на ячейке или нет.

Чтобы не писать проект с нуля, его можно загрузить по этой ссылке

Создадим в UIViewController ivar которую назовем NSArray* m_dataArray. Код генерации:

-(void) generateRandomData {

NSMutableArray* dataArray = [NSMutableArray array];

for (int i=0; i<20; i++) {
NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:
[NSString stringWithFormat:@"Object #%d",(i+1)], @"name",
[NSNumber numberWithInt:rand()%2],@"flag",
nil];
[dataArray addObject:dict];
}

m_dataArray = [dataArray retain];
}

Как видим, значение flag может быть только 1 или 0, это и будет наш критерий показа кнопки. Метод tableView:cellForRowAtIndexPath: и tableView:numberOfRowsInSection::

-(int) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

return m_dataArray.count;
}
-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 1
static NSString* cellID = @"cellID";
UITableViewCell* cell;
cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if ( !cell ) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID] autorelease];
// 2
}
// 3
NSDictionary* dict = [m_dataArray objectAtIndex:indexPath.row];
cell.textLabel.text = [dict objectForKey:@"name"];
if ( [[dict objectForKey:@"flag"] intValue] == 1 ) {
// Тут ОШИБКА, никогда так не делайте
UIButton* button = [UIButton buttonWithType:UIButtonTypeInfoDark];
button.frame = CGRectMake(320 - 18 - 20, 44/2-19/2, 18, 19);
[cell addSubview:button];
}
return cell;
}

Характерная ошибка в этом коде — то, что в части, в которой мы должны назначать свойствам ячейки значения — мы добавляем на ячейку кнопку методом addSubview! Как результат — кнопка сначала будет лишь на тех ячейках на которых нужно. Самое чудесное начинается после того как мы прокрутим таблицу — кнопка будет на всех ячейках! В этом и заключается ошибка разработчиков!



На картинках мы видим таблицу до и после прокрутки. «Почему работает не так как я хочу ?!» — искренне недоумевают разработчики, когда допускают эту ошибку.

Происходит это потому что в таблице ячейки не создаются заново, а используются повторно и показываются много раз. И если вы положите на нее в (3) кнопку (или другой контрол) — кнопка будет отображаться и на других ячейках. Мало того, кнопка добавится не один раз, а несколько!

Еще может возникнуть вопрос — а почему кнопка не на всех ячейках сразу, ведь используется одна ячейка? Я выше писал, что код создающий ячейку в данном примере вызывается 12 раз. Т.е., по-сути, мы имеем 12 ячеек. Как только самая верхняя ячейка спрячется она тут же превратиться в самую нижнюю, которая выехала.

Как решить эту проблему? Приведу наименее болезненный способ, но есть и другие.

Код создания кноки нужно перенести в (2) и сразу же ее там спрятать. И, если нужно, в (3) ее нужно показывать, а если она не нужна — прятать (свойство hidden). Вот как измениться метод tableView:cellForRowAtIndexPath::

-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

// 1
static NSString* cellID = @"cellID";
UITableViewCell* cell;
cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if ( !cell ) {
// 2
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID] autorelease];
UIButton* button = [UIButton buttonWithType:UIButtonTypeInfoDark];
button.frame = CGRectMake(320 - 18 - 20, 44/2-19/2, 18, 19);
button.tag = 1;
[cell addSubview:button];
}
// 3
UIButton* button = (UIButton*)[cell viewWithTag:1];
NSDictionary* dict = [m_dataArray objectAtIndex:indexPath.row];
cell.textLabel.text = [dict objectForKey:@"name"];
button.hidden = [[dict objectForKey:@"flag"] intValue] != 1;
return cell;
}

Еще раз подытожим — в (2) всегда должен находиться код, который создает ячейку. Сюда же и входит добавление на нее других контролов (в данном примере — кнопки). А в (3) мы только задаем свойствам значения (Тут можно сказать, что кнопка — это свойство ячейки). В (3) никогда нельзя добавлять на ячейку другие контролы!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *