Статьи

Создание приложения для списка покупок с нуля: часть 2

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


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


Удаление элементов из списка является важным дополнением с точки зрения пользовательского опыта и общего удобства использования. Добавление этой способности включает в себя:

  1. удаление элемента из свойства items контроллера представления
  2. обновление табличного представления
  3. сохранение изменений на диск

Посмотрим, как это работает на практике.

Сначала нам нужно добавить кнопку редактирования на панель навигации. В методе 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 важны для включения редактирования в табличном представлении:

  1. tableView:canEditRowAtIndexPath:
  2. 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: Второй аргумент соответствующего метода указывает, какой тип действия пользователь выполнил, вставив или удалив строку. Приложение списка покупок будет обеспечивать только удаление строк из табличного представления.

Удаление строк из табличного представления включает в себя:

  1. удаление соответствующего элемента из свойства items контроллера представления
  2. Обновление табличного представления путем удаления соответствующей строки

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


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

Не забудьте сохранить список элементов, чтобы убедиться, что изменения записаны на диск. Запустите приложение, чтобы проверить функциональность редактирования.


Прежде чем мы исследуем источник данных контроллера представления списка покупок, давайте создадим некоторые леса для работы с ними. Создайте новый подкласс UITableViewController и назовите его TSPShoppingListViewController .

Откройте файл реализации нового класса и добавьте два закрытых свойства типа NSArray к расширению класса:

  1. items , которые будут содержать полный список элементов
  2. 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 .

Запустите приложение, чтобы увидеть, все ли работает правильно. Конечно, список покупок в настоящее время пуст, и у нас нет способа добавить товары в список покупок. Давайте исправим это на следующем шаге.


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

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

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

Как все это работает? Есть три шага:

  1. контроллер представления списка покупок начинает с уведомления центра уведомлений о том, что он заинтересован в получении уведомлений с именем TSPShoppingListDidChangeNotification
  2. контроллер представления списка отправляет уведомление в центр уведомлений всякий раз, когда он обновляет список элементов
  3. когда контроллер представления списка покупок получает уведомление от центра уведомлений, он обновляет свой источник данных и представление таблицы в ответ

Прежде чем мы реализуем три шага, которые я только что описал, неплохо бы поближе познакомиться с классом NSNotificationCenter .

NSNotificationCenter говоря, NSNotificationCenter управляет NSNotificationCenter уведомлений. Объекты в приложении могут регистрироваться в центре уведомлений для получения уведомлений с использованием addObserver:selector:name:object: где

  1. первый аргумент — это объект, который будет получать уведомления (наблюдатель)
  2. селектор — это действие, которое будет запущено, когда наблюдатель получит уведомление
  3. имя это имя уведомления
  4. последний аргумент — это объект, который отправляет уведомление

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