Ячейка табличного представления не знает о табличном представлении, которому она принадлежит, и это нормально. На самом деле так и должно быть. Однако, люди, которые плохо знакомы с этой концепцией, часто смущаются этим. Например, если пользователь нажимает кнопку в ячейке табличного представления, как получить индексный путь ячейки, чтобы можно было получить соответствующую модель? В этом уроке я покажу вам, как этого не делать, как это обычно делается, и как это делать со стилем и элегантностью.
1. Введение
Когда пользователь tableView:didSelectRowAtIndexPath:
ячейки табличного представления, табличное представление вызывает tableView:didSelectRowAtIndexPath:
протокола UITableViewDelegate
на tableView:didSelectRowAtIndexPath:
табличного представления. Этот метод принимает два аргумента: табличное представление и индексный путь выбранной ячейки.
Однако проблема, которую мы собираемся решить в этом уроке, немного сложнее. Предположим, у нас есть табличное представление с ячейками, каждая ячейка содержит кнопку. При нажатии кнопки запускается действие. В действии нам нужно выбрать модель, которая соответствует позиции ячейки в табличном представлении. Другими словами, нам нужно знать путь индекса ячейки. Как мы можем вывести путь индекса ячейки, если мы получим ссылку только на нажатую кнопку? Это проблема, которую мы решим в этом уроке.
2. Настройка проекта
Шаг 1: Создать проект
Создайте новый проект в XCode, выбрав шаблон приложения Single View Application из списка шаблонов приложений iOS . Назовите проект « Блоки и ячейки» , установите « Устройства» на iPhone и нажмите « Далее» . Скажите Xcode, где вы хотите сохранить проект, и нажмите « Создать» .
Шаг 2. Обновление цели развертывания
Откройте Навигатор проектов слева, выберите проект в разделе « Проект » и установите для « Цели развертывания» значение iOS 6 . Мы делаем это, чтобы убедиться, что мы можем запустить приложение как на iOS 6, так и на iOS 7. Причина этого станет ясна позже в этом руководстве.
Шаг 3. Создание подкласса UITableViewCell
Выберите New> File … в меню File и выберите класс Objective C из списка шаблонов Cocoa Touch . Назовите класс TPSButtonCell
и убедитесь, что он наследуется от UITableViewCell
.
Откройте файл заголовка класса и объявите два выхода UILabel
экземпляр titleLabel
именем titleLabel
и экземпляр actionButton
именем actionButton
.
1
2
3
4
5
6
7
8
|
#import <UIKit/UIKit.h>
@interface TPSButtonCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIButton *actionButton;
@end
|
Шаг 4: Обновление View Controller
Откройте файл TPSViewController
класса TPSViewController
и создайте выход с именем tableView
типа UITableView
. TPSViewController
также должен принять протоколы UITableViewDataSource
и UITableViewDelegate
.
1
2
3
4
5
6
7
|
#import <UIKit/UIKit.h>
@interface TPSViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
|
Нам также нужно кратко взглянуть на файл реализации контроллера представления. Откройте TPSViewController.m и объявите статическую переменную типа NSString
которую мы будем использовать в качестве идентификатора повторного использования для ячеек в табличном представлении.
1
2
3
4
5
6
7
8
9
|
#import «TPSViewController.h»
@implementation TPSViewController
static NSString *CellIdentifier = @»CellIdentifier»;
// … //
@end
|
Шаг 5: Пользовательский интерфейс
Откройте основную раскадровку проекта Main.Storyboard и перетащите табличное представление в представление контроллера представления. Выберите табличное представление и соедините его dataSource
и delegate
выходы с экземпляром контроллера представления. Пока табличное представление еще выбрано, откройте инспектор атрибутов и установите количество ячеек прототипа равным 1
. Атрибут Content должен быть установлен в Dynamic Prototypes . Теперь вы должны увидеть одну ячейку-прототип в табличном представлении.
Выберите ячейку прототипа и установите для ее класса значение TPSButtonCell
в Инспекторе удостоверений . Когда ячейка все еще выбрана, откройте инспектор атрибутов и установите для атрибута « Стиль» значение « Пользовательский», а для идентификатора — « CellIdentifier» .
Перетащите экземпляр UILabel
из библиотеки объектов в представление содержимого ячейки и повторите этот шаг для экземпляра UIButton
. Выберите ячейку, откройте инспектор соединений и соедините titleLabel
и actionButton
с их аналогами в ячейке прототипа.
Прежде чем мы вернемся к коду, нам нужно сделать еще одно соединение. Выберите контроллер представления, еще раз откройте инспектор соединений и подключите выход tableView
контроллера представления с представлением таблицы в раскадровке. Вот и все для пользовательского интерфейса.
3. Заполнение табличного представления
Шаг 1. Создание источника данных
Давайте TPSViewController
табличное представление некоторыми известными фильмами, выпущенными в 2013 году. В классе TPSViewController
объявите свойство типа NSArray
и назовите его dataSource
. Соответствующая переменная экземпляра будет содержать фильмы, которые мы покажем в табличном представлении. dataSource
дюжиной фильмов в методе viewDidLoad
контроллера представления.
1
2
3
4
5
6
7
|
#import «TPSViewController.h»
@interface TPSViewController ()
@property (strong, nonatomic) NSArray *dataSource;
@end
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
— (void)viewDidLoad {
[super viewDidLoad];
// Setup Data Source
self.dataSource = @[
@{ @»title» : @»Gravity», @»year» : @(2013) },
@{ @»title» : @»12 Years a Slave», @»year» : @(2013) },
@{ @»title» : @»Before Midnight», @»year» : @(2013) },
@{ @»title» : @»American Hustle», @»year» : @(2013) },
@{ @»title» : @»Blackfish», @»year» : @(2013) },
@{ @»title» : @»Captain Phillips», @»year» : @(2013) },
@{ @»title» : @»Nebraska», @»year» : @(2013) },
@{ @»title» : @»Rush», @»year» : @(2013) },
@{ @»title» : @»Frozen», @»year» : @(2013) },
@{ @»title» : @»Star Trek Into Darkness», @»year» : @(2013) },
@{ @»title» : @»The Conjuring», @»year» : @(2013) },
@{ @»title» : @»Side Effects», @»year» : @(2013) },
@{ @»title» : @»The Attack», @»year» : @(2013) },
@{ @»title» : @»The Hobbit», @»year» : @(2013) },
@{ @»title» : @»We Are What We Are», @»year» : @(2013) },
@{ @»title» : @»Something in the Air», @»year» : @(2013) }
];
}
|
Шаг 2. Реализация протокола UITableViewDataSource
Реализация протокола UITableViewDataSource
очень проста. Нам нужно только реализовать numberOfSectionsInTableView:
tableView:numberOfRowsInSection:
и tableView:cellForRowAtIndexPath:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.dataSource ?
}
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataSource ?
}
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TPSButtonCell *cell = (TPSButtonCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Fetch Item
NSDictionary *item = [self.dataSource objectAtIndex:indexPath.row];
// Configure Table View Cell
[cell.titleLabel setText:[NSString stringWithFormat:@»%@ (%@)», item[@»title»], item[@»year»]]];
[cell.actionButton addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
|
В tableView:cellForRowAtIndexPath:
мы используем тот же идентификатор, который мы установили в основной раскадровке, CellIdentifier
, который мы объявили ранее в руководстве. Мы TPSButtonCell
ячейку к экземпляру TPSButtonCell
, TPSButtonCell
соответствующий элемент из источника данных и обновляем метку заголовка ячейки. Мы также добавляем цель и действие для события UIControlEventTouchUpInside
кнопки.
Не забудьте добавить оператор импорта для класса TPSButtonCell
в верхней части TPSViewController.m.
1
|
#import «TPSButtonCell.h»
|
Чтобы предотвратить сбой приложения при нажатии кнопки, реализуйте didTapButton:
как показано ниже.
1
2
3
|
— (void)didTapButton:(id)sender {
NSLog(@»%s», __PRETTY_FUNCTION__);
}
|
Создайте проект и запустите его в iOS Simulator, чтобы увидеть, что у нас есть. Вы должны увидеть список фильмов, и, нажав кнопку справа, вы получите сообщение в консоль Xcode. Отлично. Пришло время для учебника.
4. Как этого не делать
Когда пользователь нажимает кнопку справа, он отправляет сообщение didTapButton:
в контроллер представления. Вам почти всегда нужно знать путь индекса ячейки табличного представления, в которой находится кнопка. Но как вы получаете этот путь индекса? Как я уже говорил, есть три подхода, которые вы можете использовать. Давайте сначала посмотрим, как этого не делать.
Взгляните на реализацию didTapButton:
и попытайтесь выяснить, что с ней не так. Вы видите опасность? Давай я тебе помогу. Сначала запустите приложение на iOS 7, а затем на iOS 6. Посмотрите, что выводит Xcode на консоль.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
— (void)didTapButton:(id)sender {
// Find Table View Cell
UITableViewCell *cell = (UITableViewCell *)[[[sender superview] superview] superview];
// Infer Index Path
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
// Fetch Item
NSDictionary *item = [self.dataSource objectAtIndex:indexPath.row];
// Log to Console
NSLog(@»%@», item[@»title»]);
}
|
Проблема этого подхода в том, что он подвержен ошибкам. На iOS 7 этот подход работает просто отлично. На iOS 6, однако, это не работает. Чтобы он работал на iOS 6, вам нужно реализовать метод, показанный ниже. Иерархия представления ряда общих UIView
подклассов, таких как UITableView
, изменилась в iOS 7, и результат состоит в том, что вышеупомянутый подход не дает согласованного результата.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
— (void)didTapButton:(id)sender {
// Find Table View Cell
UITableViewCell *cell = (UITableViewCell *)[[sender superview] superview];
// Infer Index Path
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
// Fetch Item
NSDictionary *item = [self.dataSource objectAtIndex:indexPath.row];
// Log to Console
NSLog(@»%@», item[@»title»]);
}
|
Разве мы не можем просто проверить, работает ли устройство на iOS 7? Это очень хорошая идея. Однако что вы будете делать, когда iOS 8 снова изменит внутреннюю иерархию представления UITableView
? Собираетесь ли вы исправлять свое приложение каждый раз, когда будет выпущен основной выпуск iOS? А как насчет всех тех пользователей, которые не обновляются до последней (исправленной) версии вашего приложения? Надеюсь, понятно, что нам нужно лучшее решение.
5. Лучшее решение
Лучшим подходом является вывод пути индекса ячейки в табличном представлении на основе позиции sender
, экземпляра UIButton
, в табличном представлении. Мы используем convertPoint:toView:
для этого. Этот метод преобразует центр кнопки из системы координат кнопки в систему координат табличного представления. Тогда становится очень легко. Мы вызываем indexPathForRowAtPoint:
для табличного представления и передаем pointInSuperview
к нему. Это дает нам индексный путь, который мы можем использовать для получения правильного элемента из источника данных.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
— (void)didTapButton:(id)sender {
// Cast Sender to UIButton
UIButton *button = (UIButton *)sender;
// Find Point in Superview
CGPoint pointInSuperview = [button.superview convertPoint:button.center toView:self.tableView];
// Infer Index Path
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:pointInSuperview];
// Fetch Item
NSDictionary *item = [self.dataSource objectAtIndex:indexPath.row];
// Log to Console
NSLog(@»%@», item[@»title»]);
}
|
Этот подход может показаться громоздким, но на самом деле это не так. Это подход, на который не влияют изменения в иерархии представлений UITableView
и он может использоваться во многих сценариях, в том числе в представлениях коллекций.
6. Элегантное решение
Есть еще одно решение для решения проблемы, и оно требует немного больше работы. Результатом, однако, является отображение современного Objective-C. Начните с пересмотра файла заголовка TPSButtonCell
и объявите открытый метод с именем setDidTapButtonBlock:
который принимает блок.
01
02
03
04
05
06
07
08
09
10
|
#import <UIKit/UIKit.h>
@interface TPSButtonCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIButton *actionButton;
— (void)setDidTapButtonBlock:(void (^)(id sender))didTapButtonBlock;
@end
|
В файле реализации TPSButtonCell
создайте частное свойство с именем didTapButtonBlock
как показано ниже. Обратите внимание, что приписываемое свойство установлено для copy
, поскольку блоки должны быть скопированы, чтобы отслеживать их захваченное состояние вне исходной области.
1
2
3
4
5
6
7
|
#import «TPSButtonCell.h»
@interface TPSButtonCell ()
@property (copy, nonatomic) void (^didTapButtonBlock)(id sender);
@end
|
Вместо добавления цели и действия для события UIControlEventTouchUpInside
в UIControlEventTouchUpInside
контроллера tableView:cellForRowAtIndexPath:
мы добавляем цель и действие в awakeFromNib
в TPSButtonCell
классе TPSButtonCell
.
1
2
3
4
5
|
— (void)awakeFromNib {
[super awakeFromNib];
[self.actionButton addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];
}
|
Реализация didTapButton:
тривиально.
1
2
3
4
5
|
— (void)didTapButton:(id)sender {
if (self.didTapButtonBlock) {
self.didTapButtonBlock(sender);
}
}
|
Это может показаться большой работой для простой кнопки, но держите вас в руках, пока мы не реорганизовали tableView:cellForRowAtIndexPath:
в классе TPSViewController
. Вместо добавления цели и действия к кнопке ячейки, мы устанавливаем ячейку didTapButtonBlock
. Получить ссылку на соответствующий элемент источника данных становится очень и очень просто. Это решение, безусловно, является самым элегантным решением этой проблемы.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TPSButtonCell *cell = (TPSButtonCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Fetch Item
NSDictionary *item = [self.dataSource objectAtIndex:indexPath.row];
// Configure Table View Cell
[cell.titleLabel setText:[NSString stringWithFormat:@»%@ (%@)», item[@»title»], item[@»year»]]];
[cell setDidTapButtonBlock:^(id sender) {
NSLog(@»%@», item[@»title»]);
}];
return cell;
}
|
Вывод
Хотя концепция блоков существует уже несколько десятилетий, разработчикам Cocoa пришлось подождать до 2011 года. Блоки могут облегчить решение сложных проблем и упростить сложный код. С момента появления блоков Apple начала широко использовать их в своих собственных API-интерфейсах, поэтому я призываю вас последовать примеру Apple, используя преимущества блоков в своих собственных проектах.