Статьи

Блоки и ячейки табличного представления на iOS


Ячейка табличного представления не знает о табличном представлении, которому она принадлежит, и это нормально. На самом деле так и должно быть. Однако, люди, которые плохо знакомы с этой концепцией, часто смущаются этим. Например, если пользователь нажимает кнопку в ячейке табличного представления, как получить индексный путь ячейки, чтобы можно было получить соответствующую модель? В этом уроке я покажу вам, как этого не делать, как это обычно делается, и как это делать со стилем и элегантностью.

Когда пользователь tableView:didSelectRowAtIndexPath: ячейки табличного представления, табличное представление вызывает tableView:didSelectRowAtIndexPath: протокола UITableViewDelegate на tableView:didSelectRowAtIndexPath: табличного представления. Этот метод принимает два аргумента: табличное представление и индексный путь выбранной ячейки.

Однако проблема, которую мы собираемся решить в этом уроке, немного сложнее. Предположим, у нас есть табличное представление с ячейками, каждая ячейка содержит кнопку. При нажатии кнопки запускается действие. В действии нам нужно выбрать модель, которая соответствует позиции ячейки в табличном представлении. Другими словами, нам нужно знать путь индекса ячейки. Как мы можем вывести путь индекса ячейки, если мы получим ссылку только на нажатую кнопку? Это проблема, которую мы решим в этом уроке.

Создайте новый проект в XCode, выбрав шаблон приложения Single View Application из списка шаблонов приложений iOS . Назовите проект « Блоки и ячейки» , установите « Устройства» на iPhone и нажмите « Далее» . Скажите Xcode, где вы хотите сохранить проект, и нажмите « Создать» .

Выберите шаблон проекта
Настроить проект

Откройте Навигатор проектов слева, выберите проект в разделе « Проект » и установите для « Цели развертывания» значение iOS 6 . Мы делаем это, чтобы убедиться, что мы можем запустить приложение как на iOS 6, так и на iOS 7. Причина этого станет ясна позже в этом руководстве.

Настройка параметров проекта

Выберите New> File … в меню File и выберите класс Objective C из списка шаблонов Cocoa Touch . Назовите класс TPSButtonCell и убедитесь, что он наследуется от UITableViewCell .

Создать подкласс UITableViewCell
Создать подкласс 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

Откройте файл 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

Откройте основную раскадровку проекта Main.Storyboard и перетащите табличное представление в представление контроллера представления. Выберите табличное представление и соедините его dataSource и delegate выходы с экземпляром контроллера представления. Пока табличное представление еще выбрано, откройте инспектор атрибутов и установите количество ячеек прототипа равным 1 . Атрибут Content должен быть установлен в Dynamic Prototypes . Теперь вы должны увидеть одну ячейку-прототип в табличном представлении.

Выберите ячейку прототипа и установите для ее класса значение TPSButtonCell в Инспекторе удостоверений . Когда ячейка все еще выбрана, откройте инспектор атрибутов и установите для атрибута « Стиль» значение « Пользовательский», а для идентификатора — « CellIdentifier» .

Перетащите экземпляр UILabel из библиотеки объектов в представление содержимого ячейки и повторите этот шаг для экземпляра UIButton . Выберите ячейку, откройте инспектор соединений и соедините titleLabel и actionButton с их аналогами в ячейке прототипа.

Настройка интерфейса пользователя

Прежде чем мы вернемся к коду, нам нужно сделать еще одно соединение. Выберите контроллер представления, еще раз откройте инспектор соединений и подключите выход tableView контроллера представления с представлением таблицы в раскадровке. Вот и все для пользовательского интерфейса.

Давайте 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) }
                        ];
}

Реализация протокола 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. Отлично. Пришло время для учебника.

Когда пользователь нажимает кнопку справа, он отправляет сообщение 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? А как насчет всех тех пользователей, которые не обновляются до последней (исправленной) версии вашего приложения? Надеюсь, понятно, что нам нужно лучшее решение.

Лучшим подходом является вывод пути индекса ячейки в табличном представлении на основе позиции 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 и он может использоваться во многих сценариях, в том числе в представлениях коллекций.

Есть еще одно решение для решения проблемы, и оно требует немного больше работы. Результатом, однако, является отображение современного 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, используя преимущества блоков в своих собственных проектах.