В предыдущем сокращении мы заложили основу для приложения списка покупок. В первой части этого урока мы дополнительно доработаем приложение, позволив пользователю редактировать и удалять элементы из списка. Во второй части этого урока мы добавили возможность выбирать товары из списка для создания списка покупок.
Контур
Как я упоминал в предыдущей статье, одной из основных функций приложения списка покупок является управление списком товаров. До сих пор мы проделали большую работу, но у пользователя должна быть возможность редактировать и удалять элементы из списка, что будет основной темой первой части этого урока. Во второй части мы реализуем вторую основную функцию приложения списка покупок, создавая список покупок, выбирая товары из списка товаров.
9. Удаление элементов
Удаление элементов из списка является важным дополнением с точки зрения пользовательского опыта и общего удобства использования. Добавление этой способности включает в себя:
- удаление элемента из свойства
items
контроллера представления - обновление табличного представления
- сохранение изменений на диск
Посмотрим, как это работает на практике.
Сначала нам нужно добавить кнопку редактирования на панель навигации. В методе viewDidLoad
контроллера viewDidLoad
создайте экземпляр UIBarButtonItem
и назначьте его rightBarButtonItem
свойства navigationItem
контроллера представления.
Как и в предыдущем уроке, мы создаем элемент кнопки панели, вызывая метод initWithBarButtonSystemItem:target:action:
initialization. Первый параметр, который мы передаем — это UIBarButtonSystemItemEdit
. Кроме того, мы передаем self
в качестве цели и устанавливаем действие для editItems:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
— (void)viewDidLoad {
[super viewDidLoad];
// NSLog(@»Items > %@», self.items);
// Register Class for Cell Reuse
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
// Create Add Button
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addItem:)];
// Create Edit Button
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:@selector(editItems:)];
}
|
Реализация editItems:
action — это только одна строка кода, как вы можете видеть ниже. Всякий раз, когда пользователь нажимает кнопку редактирования, представление таблицы переключается в режим редактирования или выходит из него. Мы делаем это с помощью небольшой хитрости. Мы спрашиваем табличное представление, находится ли оно в настоящее время в режиме редактирования, которое возвращает значение BOOL
и инвертирует возвращаемое значение ( YES
становится NO
и наоборот). Метод, который мы вызываем в табличном представлении, это setEditing:animated:
который является специализированным установщиком, который принимает параметр анимации.
1
2
3
|
— (void)editItems:(id)sender {
[self.tableView setEditing:![self.tableView isEditing] animated:YES];
}
|
Если вы запустите приложение со списком покупок в iOS Simulator и коснитесь кнопки редактирования, вы увидите, что представление таблицы переключается в режим редактирования и выходит из него.
Два метода протокола UITableViewDataSource
важны для включения редактирования в табличном представлении:
-
tableView:canEditRowAtIndexPath:
-
tableView:commitEditingStyle:forRowAtIndexPath:
Когда пользователь нажимает кнопку редактирования, табличное представление запрашивает источник данных, какие строки можно редактировать, отправляя источнику данных сообщение tableView:canEditRowAtIndexPath:
Когда YES
возвращается для определенного пути индекса, табличное представление инструктирует ячейку табличного представления соответствующей строки, которую необходимо переключить в режим редактирования, в зависимости от режима редактирования табличного представления.
На практике это означает, что ячейка табличного представления показывает или скрывает свои элементы редактирования. Эту концепцию лучше всего понять, реализовав метод tableView:canEditRowAtIndexPath:
метод, как показано ниже.
1
2
3
4
5
6
7
|
— (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
if ([indexPath row] == 1) {
return NO;
}
return YES;
}
|
Вышеприведенная реализация tableView:canEditRowAtIndexPath:
позволяет пользователю редактировать каждую строку в табличном представлении, за исключением второй строки. Запустите приложение в iOS Simulator и попробуйте.
Для приложения списка покупок пользователь должен иметь возможность редактировать каждую строку в табличном представлении, что означает, что tableView:canEditRowAtIndexPath:
всегда должен возвращать YES
как показано ниже.
1
2
3
|
— (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
|
Возможно, вы заметили, что ничего не происходит, когда вы пытаетесь удалить строку в табличном представлении. Загадка еще не закончена. Всякий раз, когда пользователь нажимает кнопку удаления строки, табличное представление отправляет своему источнику данных сообщение tableView:commitEditingStyle:forRowAtIndexPath:
Второй аргумент соответствующего метода указывает, какой тип действия пользователь выполнил, вставив или удалив строку. Приложение списка покупок будет обеспечивать только удаление строк из табличного представления.
Удаление строк из табличного представления включает в себя:
- удаление соответствующего элемента из свойства
items
контроллера представления - Обновление табличного представления путем удаления соответствующей строки
Давайте tableView:commitEditingStyle:forRowAtIndexPath:
реализацию tableView:commitEditingStyle:forRowAtIndexPath:
Метод начинается с проверки, равен ли стиль редактирования UITableViewCellEditingStyleDelete
поскольку мы хотим только разрешить пользователю удалять строки из табличного представления.
Если стиль редактирования равен UITableViewCellEditingStyleDelete
, то соответствующий элемент удаляется из массива items
, соответствующая строка удаляется из табличного представления, а обновленный список элементов сохраняется на диске.
01
02
03
04
05
06
07
08
09
10
11
12
|
— (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete Item from Items
[self.items removeObjectAtIndex:[indexPath row]];
// Update Table View
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
// Save Changes to Disk
[self saveItems];
}
}
|
Запустите приложение в iOS Simulator, чтобы проверить функциональность удаления. Не забудьте выйти и перезапустить приложение, чтобы убедиться, что элементы навсегда удалены из списка.
10. Редактирование предметов
Мы могли бы повторно использовать класс TSPAddItemViewController
для редактирования элементов, но повторное использование класса контроллера представления как для добавления, так и для редактирования элементов часто может настолько UIViewController
реализацию, что я обычно заканчиваю тем, что создаю отдельный подкласс UIViewController
для редактирования элементов. Первоначально это приведет к некоторому совпадению с точки зрения реализации и пользовательского интерфейса, но это даст нам большую гибкость в будущем.
Создание редактирующего контроллера вида
Создайте новый подкласс UIViewController
и назовите его TSPEditItemViewController
. Заголовочные файлы классов TSPAddItemViewController
и TSPEditItemViewController
очень похожи.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
#import <UIKit/UIKit.h>
@class TSPItem;
@protocol TSPEditItemViewControllerDelegate;
@interface TSPEditItemViewController : UIViewController
@property IBOutlet UITextField *nameTextField;
@property IBOutlet UITextField *priceTextField;
@property TSPItem *item;
@property (weak) id<TSPEditItemViewControllerDelegate> delegate;
@end
@protocol TSPEditItemViewControllerDelegate <NSObject>
— (void)controller:(TSPEditItemViewController *)controller didUpdateItem:(TSPItem *)item;
@end
|
После импорта заголовков инфраструктуры UIKit добавляется предварительное объявление класса и протокола. Интерфейс класса аналогичен интерфейсу класса TSPAddItemViewController
. Основным отличием является объявление свойства для хранения редактируемого элемента.
Заголовочный файл заканчивается объявлением протокола TSPEditItemViewControllerDelegate
. Объявление протокола содержит один метод, который вызывается при обновлении элемента. Метод принимает контроллер представления и обновленный элемент в качестве аргументов.
Откройте основную раскадровку, перетащите экземпляр UIViewController
из библиотеки объектов , установите для его класса значение TSPEditItemViewController
и создайте ручной переход от контроллера представления списка к контроллеру представления элемента редактирования, назвав его EditItemViewController
.
Перетащите два экземпляра UITextField
из библиотеки объектов в представление контроллера представления и расположите их, как показано на рисунке ниже. Выберите верхнее текстовое поле, откройте инспектор атрибутов и введите « Имя» в поле « Заполнитель» . Выделите нижнее текстовое поле и в инспекторе атрибутов установите для его текста-заполнителя значение « Цена» и установите для « Клавиатура» значение « Цифровая панель» . Выберите объект контроллера представления, откройте инспектор соединений и подключите nameTextField
и priceTextField
к соответствующему текстовому полю в пользовательском интерфейсе.
В методе viewDidLoad
контроллера viewDidLoad
создайте кнопку сохранения, как мы делали в классе TSPAddItemViewController
.
1
2
3
4
5
6
|
— (void)viewDidLoad {
[super viewDidLoad];
// Create Save Button
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(save:)];
}
|
Реализация действия save:
очень похожа на ту, что мы реализовали в классе TSPAddItemViewController
. Однако есть несколько тонких отличий, на которые я хочу обратить внимание.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
— (void)save:(id)sender {
NSString *name = [self.nameTextField text];
float price = [[self.priceTextField text] floatValue];
// Update Item
[self.item setName:name];
[self.item setPrice:price];
// Notify Delegate
[self.delegate controller:self didUpdateItem:self.item];
// Pop View Controller
[self.navigationController popViewControllerAnimated:YES];
}
|
Вместо того, чтобы передавать значения имени и цены делегату, мы напрямую обновляем элемент и передаем обновленный элемент делегату контроллера представления. Поскольку контроллер представления является дочерним контроллером представления контроллера навигации, мы отклоняем контроллер представления, выталкивая его из стека навигации.
Отображение контроллера представления элемента редактирования
Через несколько минут мы реализуем функциональность, которая позволяет пользователю выбирать элементы из контроллера представления списка, чтобы добавить их в список покупок. Пользователь сможет сделать это, нажав строку в представлении списка. Тогда возникает вопрос, каким образом пользователь сможет редактировать товар, если нажатие на строку зарезервировано для добавления товара в список покупок?
Инфраструктура UIKit предоставляет кнопку подробного раскрытия информации именно для этого варианта использования. Кнопка раскрытия подробностей — это маленький синий шеврон, расположенный справа от ячейки таблицы. Чтобы добавить кнопку подробного раскрытия в ячейку табличного представления, нам нужно повторно посетить tableView:cellForRowAtIndexPath:
метод в контроллере представления списка и изменить его, как показано ниже.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Dequeue Reusable Cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Fetch Item
TSPItem *item = [self.items objectAtIndex:[indexPath row]];
// Configure Cell
[cell.textLabel setText:[item name]];
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
return cell;
}
|
Каждая ячейка табличного представления имеет свойство accessoryType
. В приведенной ниже реализации мы установили для него значение UITableViewCellAccessoryDetailDisclosureButton
. Как вы могли заметить, инженеры Apple не любят короткие имена.
Как табличное представление уведомляет делегата о нажатии кнопки раскрытия информации? Неудивительно, что протокол UITableViewDelegate
определяет tableView:accessoryButtonTappedForRowWithIndexPath:
метод для этой цели. Посмотрите на его реализацию.
01
02
03
04
05
06
07
08
09
10
11
12
|
— (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
// Fetch Item
TSPItem *item = [self.items objectAtIndex:[indexPath row]];
if (item) {
// Update Selection
[self setSelection:item];
// Perform Segue
[self performSegueWithIdentifier:@»EditItemViewController» sender:self];
}
}
|
Мы выбираем правильный элемент из свойства items
и сохраняем его в selection
, частном свойстве контроллера представления списка, которое мы объявим через минуту. Затем мы выполняем переход с идентификатором EditItemViewController
.
Прежде чем мы обновим реализацию prepareForSegue:sender:
нам нужно объявить приватное свойство для хранения выбранного элемента.
01
02
03
04
05
06
07
08
09
10
|
#import «TSPListViewController.h»
#import «TSPItem.h»
@interface TSPListViewController ()
@property NSMutableArray *items;
@property TSPItem *selection;
@end
|
Нам также необходимо импортировать файл TSPEditItemViewController
класса TSPEditItemViewController в TSPListViewController.h и согласовать класс TSPEditItemViewControllerDelegate
протоколом TSPEditItemViewControllerDelegate
.
1
2
3
4
5
6
7
8
|
#import <UIKit/UIKit.h>
#import «TSPAddItemViewController.h»
#import «TSPEditItemViewController.h»
@interface TSPListViewController : UITableViewController <TSPAddItemViewControllerDelegate, TSPEditItemViewControllerDelegate>
@end
|
Обновленная реализация prepareForSegue:sender:
довольно проста, как вы можете видеть ниже. Мы получаем ссылку на контроллер представления элемента edit и устанавливаем его delegate
и свойства item
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
— (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@»AddItemViewController»]) {
// Destination View Controller
UINavigationController *nc = (UINavigationController *)segue.destinationViewController;
// Fetch Add Item View Controller
TSPAddItemViewController *vc = [nc.viewControllers firstObject];
// Set Delegate
[vc setDelegate:self];
} else if ([segue.identifier isEqualToString:@»EditItemViewController»]) {
// Fetch Edit Item View Controller
TSPEditItemViewController *vc = (TSPEditItemViewController *)segue.destinationViewController;
// Set Delegate
[vc setDelegate:self];
[vc setItem:self.selection];
}
}
|
Реализация класса TSPEditItemViewController
почти завершена. В его методе viewDidLoad
мы заполняем текстовые поля данными свойства item
. Поскольку метод setText:
для текстового поля принимает только экземпляр NSString
, нам нужно обернуть значение float
свойства price
элемента в строку, чтобы отобразить его в priceTextField
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
— (void)viewDidLoad {
[super viewDidLoad];
// Create Save Button
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(save:)];
// Populate Text Fields
if (self.item) {
[self.nameTextField setText:[self.item name]];
[self.priceTextField setText:[NSString stringWithFormat:@»%f», [self.item price]]];
}
}
|
Принятие протокола делегата
Принятие протокола TSPEditItemViewControllerDelegate
ограничено реализацией controller:didUpdateItem:
метод, как показано ниже. Вас может удивить, что мы не обновляем источник данных табличного представления. Все, что мы делаем в методе делегата, это перезагрузим одну строку табличного представления.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
— (void)controller:(TSPEditItemViewController *)controller didUpdateItem:(TSPItem *)item {
// Fetch Item
for (int i = 0; i < [self.items count]; i++) {
TSPItem *anItem = [self.items objectAtIndex:i];
if ([[anItem uuid] isEqualToString:[item uuid]]) {
// Update Table View Row
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
// Save Items
[self saveItems];
}
|
Причина, по которой нам не нужно обновлять источник данных табличного представления, массив items
, заключается в том, что обновленный элемент был передан по ссылке в контроллер представления редактирования элемента. Другими словами, объект, который обновил контроллер представления элемента редактирования, является тем же объектом, который содержится в массиве items
. Это одно из приятных преимуществ работы с объектами и указателями.
Не забудьте сохранить список элементов, чтобы убедиться, что изменения записаны на диск. Запустите приложение, чтобы проверить функциональность редактирования.
11. Создание контроллера просмотра списка покупок
Прежде чем мы исследуем источник данных контроллера представления списка покупок, давайте создадим некоторые леса для работы с ними. Создайте новый подкласс UITableViewController
и назовите его TSPShoppingListViewController
.
Откройте файл реализации нового класса и добавьте два закрытых свойства типа NSArray
к расширению класса:
-
items
, которые будут содержать полный список элементов -
shoppingList
, который будет содержать только элементы списка покупок
Так как мы будем переопределять методы установки обоих свойств, мы должны объявить свойства неатомными. Не беспокойтесь о точном значении этого сейчас.
1
2
3
4
5
6
7
8
|
#import <UIKit/UIKit.h>
@interface TSPShoppingListViewController : UITableViewController
@property (nonatomic) NSArray *items;
@property (nonatomic) NSArray *shoppingList;
@end
|
Идея состоит в том, чтобы загружать список элементов каждый раз, когда в него inShoppingList
изменения, а затем анализировать список элементов и извлекать только те элементы, для которых inShoppingList
установлено значение YES
. Эти элементы будут добавлены в массив shoppingList
.
Другой вариант — хранить список покупок в отдельном файле, но недостатком этого подхода будет то, что мы должны синхронизировать элементы в контроллере представления списка и элементы в списке покупок. Это напрашивается на неприятности, если вы спросите меня.
Пользовательские сеттеры
Методы установки items
и shoppingList
сделают большую часть тяжелой работы для нас. Посмотрите на реализацию setItems:
Всякий раз, когда _items
экземпляра _items
обновляется (устанавливается с новым значением), метод buildShoppingList
вызывается на контроллере представления.
1
2
3
4
5
6
7
8
|
— (void)setItems:(NSArray *)items {
if (_items != items) {
_items = items;
// Build Shopping List
[self buildShoppingList];
}
}
|
Давайте посмотрим, как buildShoppingList
метод buildShoppingList
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
— (void)buildShoppingList {
NSMutableArray *buffer = [[NSMutableArray alloc] init];
for (int i = 0; i < [self.items count]; i++) {
TSPItem *item = [self.items objectAtIndex:i];
if ([item inShoppingList]) {
// Add Item to Buffer
[buffer addObject:item];
}
}
// Set Shopping List
self.shoppingList = [NSArray arrayWithArray:buffer];
}
|
Метод buildShoppingList
заполняет массив shoppingList
элементами, для которых свойству inShoppingList
значение YES
. В методе buildShoppingList
мы создаем изменяемый массив для временного хранения элементов списка покупок. Затем мы перебираем список элементов и добавляем каждый элемент в изменяемый массив, у inShoppingList
свойство inShoppingList
YES
. Наконец, мы устанавливаем свойство shoppingList
с содержимым изменяемого массива.
Не забудьте импортировать заголовочный файл класса TSPItem
вверху.
1
|
#import «TSPItem.h»
|
Мы также переопределяем метод setShoppingList
для перезагрузки табличного представления при каждом изменении или обновлении массива shoppingList
.
1
2
3
4
5
6
7
8
|
— (void)setShoppingList:(NSArray *)shoppingList {
if (_shoppingList != shoppingList) {
_shoppingList = shoppingList;
// Reload Table View
[self.tableView reloadData];
}
}
|
Отображение списка покупок
Реализация методов протокола UITableViewDataSource
должна быть детской игрой на данный момент. Посмотрите на реализацию каждого метода ниже.
1
2
3
|
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
|
1
2
3
|
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.shoppingList count];
}
|
01
02
03
04
05
06
07
08
09
10
11
12
|
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Dequeue Reusable Cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Fetch Item
TSPItem *item = [self.shoppingList objectAtIndex:[indexPath row]];
// Configure Cell
[cell.textLabel setText:[item name]];
return cell;
}
|
Не забудьте объявить идентификатор повторного использования ячейки и зарегистрировать класс UITableViewCell
как мы это делали в классе TSPListViewController
.
1
2
3
|
@implementation TSPShoppingListViewController
static NSString *CellIdentifier = @»Cell Identifier»;
|
1
2
3
4
5
6
|
— (void)viewDidLoad {
[super viewDidLoad];
// Register Class for Cell Reuse
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
}
|
Загрузка предметов
Как я объяснил ранее, ключевое преимущество использования списка товаров для хранения и построения списка покупок состоит в том, что приложение хранит каждый товар только в одном месте. Это делает обновление элементов в списке и в списке покупок намного меньше головной боли. Метод loadItems
идентичен методу, который мы реализовали в классе TSPListViewController
.
01
02
03
04
05
06
07
08
09
10
|
— (void)loadItems {
NSString *filePath = [self pathForItems];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
// self.items = [NSMutableArray arrayWithContentsOfFile:filePath];
self.items = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
} else {
self.items = [NSMutableArray array];
}
}
|
Конечно, метод pathForItems
идентичен, так как мы загружаем список элементов из того же файла, что и в классе TSPListViewController
.
1
2
3
4
5
6
|
— (NSString *)pathForItems {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documents = [paths lastObject];
return [documents stringByAppendingPathComponent:@»items.plist»];
}
|
Когда вы обнаружите, что дублируете код, вы должны услышать сигнал тревоги. Дублирование кода не вызывает проблем, пока вы реализуете новую функцию. Однако после этого вам следует рассмотреть возможность рефакторинга вашего кода, чтобы минимизировать количество дублирования в кодовой базе приложения. Это очень важная концепция в разработке программного обеспечения, и ее часто называют СУХОЙ , не повторяйте себя . Крис Питерс написал отличную статью на Tuts + о DRY-программировании.
Прежде чем мы TSPShoppingListViewController
класс TSPShoppingListViewController
, нам нужно обновить метод initWithCoder:
класса. Помимо установки заголовка контроллера представления, мы также загружаем список предметов, который автоматически заполняет массив shoppingList
как мы видели ранее.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
— (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// Set Title
self.title = @»Shopping List»;
// Load Items
[self loadItems];
}
return self;
}
|
Последний шаг — инициализация контроллера представления списка покупок путем обновления основной раскадровки. Это включает добавление экземпляра UITableViewController
, установку его класса в TSPShoppingListViewController
, встраивание его в контроллер навигации и создание перехода между контроллером панели вкладок и контроллером навигации. Назовите следующий ShoppingListViewController
. Выберите табличное представление контроллера представления списка покупок и установите число ячеек прототипа равным 0
.
Запустите приложение, чтобы увидеть, все ли работает правильно. Конечно, список покупок в настоящее время пуст, и у нас нет способа добавить товары в список покупок. Давайте исправим это на следующем шаге.
11. Добавление товаров в список покупок
Как я писал ранее, идея состоит в том, чтобы добавить элемент в список покупок при его нажатии в контроллере представления списка. Чтобы улучшить взаимодействие с пользователем, если элемент присутствует в списке покупок, мы показываем зеленую галочку слева от названия элемента. Если элемент, который уже находится в списке покупок, коснулся, он удаляется из списка покупок, и зеленая галочка исчезает. Это означает, что нам нужно взглянуть на tableView:didSelectRowAtIndexPath:
метод протокола UITableViewDelegate
.
Прежде чем мы реализуем tableView:didSelectRowAtIndexPath:
загрузите исходные файлы этого урока. В папке с именем Resources найдите файлы с именами checkmark.png и [email protected] и добавьте оба этих файла в проект. Они понадобятся нам через несколько минут.
В первой строке tableView:didSelectRowAtIndexPath:
мы отправляем табличному представлению сообщение tableView:didSelectRowAtIndexPath:
для tableView:didSelectRowAtIndexPath:
строки, которую нажал пользователь. При каждом нажатии на строку она должна выделяться только на мгновение, отсюда и это добавление. Затем мы выбираем соответствующий элемент для выбранной строки и обновляем свойство inShoppingList
этого элемента ( YES
становится NO
и наоборот).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
— (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// Fetch Item
TSPItem *item = [self.items objectAtIndex:[indexPath row]];
// Update Item
[item setInShoppingList:![item inShoppingList]];
// Update Cell
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if ([item inShoppingList]) {
[cell.imageView setImage:[UIImage imageNamed:@»checkmark»]];
} else {
[cell.imageView setImage:nil];
}
// Save Items
[self saveItems];
}
|
На основании значения свойства setInShoppingList
мы либо показываем, либо скрываем зеленую галочку. Мы показываем галочку, устанавливая свойство image
свойства imageView
ячейки табличного представления. Каждый экземпляр UITableViewCell
содержит представление изображения слева (экземпляр класса UIImageView
). Если установить для свойства изображения представления image
значение nil
, представление изображения будет пустым, без изображения.
Реализация tableView:didSelectRowAtIndexPath:
заканчивается сохранением списка элементов на диске, чтобы убедиться, что изменения являются постоянными.
Как список покупок узнает, когда пользователь нажимает на элемент в контроллере представления списка? Контроллер представления списка покупок не будет автоматически обновлять свое представление таблицы, если было внесено изменение в список элементов в контроллере представления списка. Кроме того, мы не хотим, чтобы контроллер представления списка и контроллер представления списка покупок взаимодействовали напрямую друг с другом, чтобы предотвратить любую форму жесткой связи.
Одним из решений этой проблемы является использование уведомлений. Всякий раз, когда контроллер представления списка вносит изменения в список элементов, он отправляет уведомление с определенным именем в центр уведомлений, объект, который управляет уведомлениями. Объекты, которые заинтересованы в определенных уведомлениях, могут добавлять себя в качестве наблюдателей этих уведомлений, что означает, что они могут отвечать, когда эти уведомления публикуются в центре уведомлений.
Как все это работает? Есть три шага:
- контроллер представления списка покупок начинает с уведомления центра уведомлений о том, что он заинтересован в получении уведомлений с именем
TSPShoppingListDidChangeNotification
- контроллер представления списка отправляет уведомление в центр уведомлений всякий раз, когда он обновляет список элементов
- когда контроллер представления списка покупок получает уведомление от центра уведомлений, он обновляет свой источник данных и представление таблицы в ответ
Прежде чем мы реализуем три шага, которые я только что описал, неплохо бы поближе познакомиться с классом NSNotificationCenter
.
NSNotificationCenter
говоря, NSNotificationCenter
управляет NSNotificationCenter
уведомлений. Объекты в приложении могут регистрироваться в центре уведомлений для получения уведомлений с использованием addObserver:selector:name:object:
где
- первый аргумент — это объект, который будет получать уведомления (наблюдатель)
- селектор — это действие, которое будет запущено, когда наблюдатель получит уведомление
- имя это имя уведомления
- последний аргумент — это объект, который отправляет уведомление
Если последний аргумент имеет значение nil
, наблюдатель получает все уведомления с указанным именем.
Получение уведомлений
TSPShoppingListViewController
initWithCoder:
метод класса TSPShoppingListViewController
и добавьте экземпляр контроллера представления в качестве наблюдателя для получения уведомлений с именем TSPShoppingListDidChangeNotification
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
— (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// Set Title
self.title = @»Shopping List»;
// Add Observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateShoppingList:) name:@»TSPShoppingListDidChangeNotification» object:nil];
}
return self;
}
|
Действие запускается, когда контроллер представления получает уведомление с таким именем updateShoppingList:
Мы установили для object
значение nil
поскольку на самом деле не имеет значения, какой объект отправил уведомление.
Прежде чем мы реализуем updateShoppingList:
нам нужно изменить метод viewDidLoad
контроллера представления списка покупок. В viewDidLoad
мы загружаем элементы с диска.
1
2
3
4
5
6
7
8
9
|
— (void)viewDidLoad {
[super viewDidLoad];
// Load Items
[self loadItems];
// Register Class for Cell Reuse
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
}
|
Отвечая на уведомления
Метод, который запускается, когда наблюдатель получает уведомление, имеет определенный формат, как вы можете видеть ниже. Он принимает один аргумент, то есть объект уведомления, который имеет тип NSNotification
. Объект уведомления содержит ссылку на объект, который разместил уведомление, и может также содержать словарь с дополнительной информацией. Реализация метода updateShoppingList:
довольно проста, то есть метод loadItems
вызывается на контроллере представления, что означает, что список элементов загружается с диска. Остальное происходит автоматически благодаря пользовательским методам установки, которые мы реализовали ранее
1
2
3
4
|
— (void)updateShoppingList:(NSNotification *)notification {
// Load Items
[self loadItems];
}
|
Отправка уведомлений
Третий фрагмент головоломки — это публикация уведомления всякий раз, когда список элементов изменяется контроллером представления списка. Мы можем сделать это в методе TSPListViewController
класса TSPListViewController
.
1
2
3
4
5
6
7
|
— (void)saveItems {
NSString *filePath = [self pathForItems];
[NSKeyedArchiver archiveRootObject:self.items toFile:filePath];
// Post Notification
[[NSNotificationCenter defaultCenter] postNotificationName:@»TSPShoppingListDidChangeNotification» object:self];
}
|
Сначала мы запрашиваем ссылку на центр уведомлений по умолчанию, вызывая defaultCenter
NSNotificationCenter
класса NSNotificationCenter
. Затем мы вызываем postNotificationName:object:
в центре уведомлений по умолчанию и передаем имя уведомления, TSPShoppingListDidChangeNotification
и объект, отправляющий уведомление.
Перед созданием проекта обязательно tableView:cellForRowAtIndexPath:
как показано ниже, чтобы отобразить зеленую галочку для элементов, которые уже присутствуют в списке покупок.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Dequeue Reusable Cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Fetch Item
TSPItem *item = [self.items objectAtIndex:[indexPath row]];
// Configure Cell
[cell.textLabel setText:[item name]];
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
// Show/Hide Checkmark
if ([item inShoppingList]) {
[cell.imageView setImage:[UIImage imageNamed:@»checkmark»]];
} else {
[cell.imageView setImage:nil];
}
return cell;
}
|
Запустите приложение со списком покупок, чтобы раскрутить его. Вы заметили, что изменения в предмете автоматически отражаются в списке покупок?
Готов к публикации?
Отлично. Где кнопка для публикации приложения со списком покупок в App Store? Мы еще не совсем закончили. Несмотря на то, что мы заложили основу приложения списка покупок, оно не готово к публикации. Есть также несколько вещей, чтобы рассмотреть.
Масштабируемость
Приложение списка покупок представляет собой скромную реализацию списка покупок. Если бы приложение содержало сотни или тысячи элементов, было бы целесообразно добавить возможности поиска, а также разделы для сортировки элементов по алфавиту (как мы делали ранее в этой серии). Важно понимать, что список элементов записывается на диск полностью при каждом обновлении элемента. Это не проблема, если список небольшой, но это произойдет, если список со временем увеличится до сотен или тысяч элементов.
Отношения
Кроме того, пользователи могут захотеть сохранить более одного списка покупок. Как бы вы справились с этим? Один из вариантов — хранить каждый список покупок в отдельном файле, но как вы будете реагировать на изменения, внесенные в товары? Собираетесь ли вы обновить каждый список покупок, который содержит товар? Когда вы начинаете работать со связями, лучше выбрать хранилище данных SQLite. Core Data — отличный компаньон, если вы решите пойти по этому пути. Это мощный фреймворк, обладающий множеством функций, которые делают большой объем кода в нашем приложении списка покупок устаревшим. Это правда, что Core Data приносит с собой немного больше накладных расходов, поэтому важно сначала рассмотреть вопрос о том, подходят ли Core Data для вашего приложения, другими словами, стоит ли это накладных расходов.
Дополнительные возможности
Существует также большой потенциал для дополнительных функций. Свойство цены товаров остается неиспользованным в текущей реализации приложения списка покупок. Кроме того, было бы неплохо, если бы пользователь мог отмечать товар из списка покупок, нажав на товар. Как видите, текущая реализация приложения со списком покупок — лишь скромное начало.
Вывод
Несмотря на то, что приложение со списком покупок еще не совсем готово для App Store, вы не можете отрицать, что оно работает так, как мы планировали, и оно показало вам много новых аспектов разработки Какао, таких как уведомления и реализация пользовательского делегата. протокол.
Теперь вы знаете, чего ожидать от iOS SDK и на что похожа разработка iOS. Вам решать, хотите ли вы продолжить свое путешествие и стать опытным разработчиком iOS. Если вы решите продолжить разработку для iOS, я предоставлю вам несколько полезных ресурсов в следующей и последней части этой серии.