В следующих двух уроках мы применим знания, полученные в этой серии, на практике, создав базовое приложение со списком покупок. Попутно вы также изучите ряд новых концепций и шаблонов, таких как создание пользовательского класса модели и реализация пользовательского шаблона делегата. У нас есть много возможностей, так что давайте начнем.
Контур
Приложение для создания списка покупок, которое мы собираемся создать, имеет две основные функции: управление списком товаров и создание списка покупок путем выбора одного или нескольких товаров из списка товаров.
Мы построим приложение на контроллере панели вкладок, чтобы переключаться между двумя представлениями быстро и просто. В этом уроке мы сосредоточимся на первой основной функции — списке элементов.
На следующем уроке мы добавим последние штрихи к списку предметов и увеличим список покупок, вторую основную функцию приложения.
Несмотря на то, что приложение со списком покупок не является сложным с точки зрения пользователя, в процессе его разработки нам необходимо принять несколько решений. Какой тип магазина мы будем использовать для хранения списка товаров? Может ли пользователь добавлять, редактировать и удалять элементы в списке? Это вопросы, которые мы рассмотрим в следующих двух уроках.
В этом уроке я также покажу вам, как заполнить приложение списка покупок фиктивными данными, чтобы дать новым пользователям что-то для начала. Заполнение приложения данными часто является отличной идеей, чтобы помочь новым пользователям быстро освоиться.
1. Создание проекта
Запустите Xcode и создайте новый проект iOS на основе шаблона проекта Empty Application .
Назовите список покупок проекта и введите название организации, идентификатор компании и префикс класса. Установите для устройства значение « iPhone» и убедитесь, что флажок « Использовать основные данные» снят. Сообщите Xcode, где сохранить проект, и нажмите « Создать» .
2. Создание раскадровки
Создайте новую раскадровку, выбрав New> File … в меню File . Выберите раскадровку из списка шаблонов интерфейса пользователя iOS .
Установите для семейства устройств iPhone и назовите раскадровку Главный
Выберите проект в Навигаторе проектов , выберите цель Список покупок из списка целей и установите для Main Interface значение Main.storyboard или Main .
3. Создание контроллера представления списка
Как и следовало ожидать, контроллер представления списка будет подклассом UITableViewController
. Создайте новый класс Objective-C, выбрав New> File … в меню File .
Назовите класс TSPListViewController
и сделайте его подклассом UITableViewController
. Сообщите Xcode, где вы хотите сохранить файлы нового класса, и нажмите « Создать» .
Откройте раскадровку, перетащите UITabBarController
экземпляр из библиотеки объектов и удалите два контроллера представления, которые связаны с контроллером панели вкладок. Перетащите UITableViewController из библиотеки объектов , установите для его класса значение TSPListViewController
в Identity Inspector справа и создайте переходную связь между контроллером панели вкладок и контроллером представления списка. Вы можете прочитать это предложение еще раз.
Контроллер представления списка должен быть корневым контроллером представления контроллера навигации. Выберите контроллер представления списка и выберите « Встроить»> «Контроллер навигации» в меню « Редактор» .
Выберите табличное представление контроллера представления списка и установите число Ячеек Прототипа в Инспекторе Атрибутов равным 0
.
Перед запуском приложения в iOS Simulator откройте TSPAppDelegate.h и обновите реализацию application:didFinishLaunchingWithOptions:
как показано ниже. Нет необходимости создавать экземпляр UIWindow
поскольку раскадровка позаботится об этом за нас.
1
2
3
|
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}
|
Запустите приложение в iOS Simulator, чтобы увидеть, все ли работает как положено. Вы должны увидеть пустой табличный вид с панелью навигации вверху и панелью вкладок внизу.
4. Создание класса модели изделия
Как мы будем работать с предметами в приложении для покупок? Другими словами, какой тип объекта мы будем использовать для хранения свойств элемента, таких как его имя, цена и строка, которая уникально идентифицирует каждый элемент?
Наиболее очевидный выбор — сохранить свойства элемента в словаре ( NSDictionary
). Даже если это будет работать нормально, это сильно ограничит и замедлит нас по мере того, как приложение станет более сложным.
Для приложения списка покупок мы собираемся создать собственный класс модели. Для настройки требуется немного больше работы, но это значительно облегчит разработку в будущем.
Создайте новый класс Objective C с именем TSPItem
и сделайте его подклассом NSObject
. Сообщите Xcode, где сохранить класс, и нажмите « Создать» .
свойства
Добавьте четыре свойства в файл заголовка нового класса модели:
-
uuid
типаNSString
для уникальной идентификации каждого элемента -
name
также типаNSString
-
price
типаfloat
-
inShoppingList
типаBOOL
чтобы указать, присутствует ли элемент в списке покупок
Важно, чтобы класс TSPItem
соответствовал протоколу NSCoding
. Причина этого станет ясна через несколько минут. Взгляните на полный файл заголовка (комментарии опущены), чтобы убедиться, что мы находимся на той же странице.
01
02
03
04
05
06
07
08
09
10
|
#import <Foundation/Foundation.h>
@interface TSPItem : NSObject <NSCoding>
@property NSString *uuid;
@property NSString *name;
@property float price;
@property BOOL inShoppingList;
@end
|
Архивирование
Одна из стратегий сохранения пользовательских объектов, таких как экземпляры класса TSPItem
, на диск — это процесс, известный как архивация. Мы будем использовать NSKeyedArchiver
и NSKeyedUnarchiver
для архивирования и разархивирования экземпляров класса TSPItem
.
Оба класса определены в платформе Foundation, как указывает их префикс класса ( NS
). Класс NSKeyedArchiver
принимает набор объектов и сохраняет их на диск в виде двоичных данных. Дополнительным преимуществом этого подхода является то, что двоичные файлы обычно меньше, чем обычные текстовые файлы, содержащие ту же информацию.
Если мы хотим использовать NSKeyedArchiver
и NSKeyedUnarchiver
для архивирования и разархивирования экземпляров класса TSPItem
, последний должен принять протокол NSCoding
, как мы указали в заголовочном файле класса.
Помните из урока о платформе Foundation, протокол NSCoding
объявляет два метода, которые класс должен реализовать, чтобы разрешить кодирование и декодирование экземпляров класса. Посмотрим, как это работает.
Если вы создаете пользовательские классы, то вы несете ответственность за указание того, как должны быть закодированы экземпляры класса (преобразованы в двоичные данные).
кодирование
В encodeWithCoder:
класс, соответствующий протоколу NSCoding
, указывает, как следует кодировать экземпляры класса. Посмотрите на реализацию ниже. Используемые нами ключи не так важны, но вы, как правило, хотите использовать имена свойств для ясности.
1
2
3
4
5
6
|
— (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.uuid forKey:@»uuid»];
[coder encodeObject:self.name forKey:@»name»];
[coder encodeFloat:self.price forKey:@»price»];
[coder encodeBool:self.inShoppingList forKey:@»inShoppingList»];
}
|
Декодирование
Всякий раз, когда закодированный объект необходимо преобразовать обратно в экземпляр соответствующего класса, ему отправляется сообщение initWithCoder:
Те же ключи, которые мы использовали в encodeWithCoder:
используются в initWithCoder:
Это очень важно.
01
02
03
04
05
06
07
08
09
10
11
12
|
— (id)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (self) {
[self setUuid:[decoder decodeObjectForKey:@»uuid»]];
[self setName:[decoder decodeObjectForKey:@»name»]];
[self setPrice:[decoder decodeFloatForKey:@»price»]];
[self setInShoppingList:[decoder decodeBoolForKey:@»inShoppingList»]];
}
return self;
}
|
Вам никогда не нужно вызывать любой из этих методов напрямую. Они вызываются только операционной системой. Соответствуя классу NSCoding
протоколу NSCoding
, мы только сообщаем операционной системе, как кодировать и декодировать экземпляры класса.
Создание экземпляров
Чтобы упростить создание новых экземпляров класса TSPItem
, мы создаем собственный метод класса. Это совершенно необязательно, но это облегчит разработку, как вы увидите позже в этом уроке.
Откройте файл TSPItem
класса TSPItem
и добавьте следующее объявление метода. Знак +
предшествующий объявлению метода, указывает, что это метод класса, а не метод экземпляра.
1
|
+ (TSPItem *)createItemWithName:(NSString *)name andPrice:(float)price;
|
Реализация метода класса содержит один новый элемент, класс NSUUID
. В createItemWithName:andPrice:
мы начинаем с создания нового экземпляра класса с последующей настройкой нового экземпляра путем установки его свойств. По умолчанию новый элемент отсутствует в списке покупок, поэтому мы установили для свойства inShoppingList
значение NO
.
Установка свойства uuid
выполняется путем NSUUID
класса NSUUID
для экземпляра класса и запроса возвращенного экземпляра для строки uuid. Как я уже сказал, важно, чтобы мы могли однозначно идентифицировать каждый экземпляр класса TSPItem
. UUID будет выглядеть примерно так: 90A0CC77-35BA-4C09-AC28-D196D991B50D .
01
02
03
04
05
06
07
08
09
10
11
12
|
+ (TSPItem *)createItemWithName:(NSString *)name andPrice:(float)price {
// Initialize Item
TSPItem *item = [[TSPItem alloc] init];
// Configure Item
[item setName:name];
[item setPrice:price];
[item setInShoppingList:NO];
[item setUuid:[[NSUUID UUID] UUIDString]];
return item;
}
|
5. Загрузка и сохранение предметов
Постоянство данных будет ключевым в нашем приложении со списком покупок, поэтому давайте посмотрим, как это реализовать. Откройте файл реализации TSPListViewController
, добавьте частное свойство типа NSMutableArray
и NSMutableArray
имя items
.
1
2
3
4
5
6
7
|
#import «TSPListViewController.h»
@interface TSPListViewController ()
@property NSMutableArray *items;
@end
|
Элементы, отображаемые в табличном представлении контроллера представления, будут сохранены в items
. Важно, чтобы items
были изменяемым массивом, потому что мы добавим возможность добавлять новые элементы чуть позже в этом уроке.
В методе инициализации класса мы загружаем список элементов с диска и сохраняем его в свойстве private items
которое мы объявили несколько минут назад. Мы также устанавливаем заголовок контроллера представления в Items, как показано ниже.
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 = @»Items»;
// Load Items
[self loadItems];
}
return self;
}
|
Метод loadItems
контроллера представления — не что иное как вспомогательный метод, чтобы сохранить initWithCoder:
класса initWithCoder:
метод кратким и читабельным. Давайте посмотрим на реализацию loadItems
.
Загрузка предметов
Метод loadItems
начинается с получения пути к файлу, в котором хранится список элементов. Мы делаем это, вызывая pathForItems
, еще один вспомогательный метод, который мы рассмотрим через несколько минут.
1
2
3
4
5
6
7
8
9
|
— (void)loadItems {
NSString *filePath = [self pathForItems];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
self.items = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
} else {
self.items = [NSMutableArray array];
}
}
|
Класс NSFileManager
в следующей строке кода — это класс, с которым мы еще не работали. Класс NSFileManager
предоставляет простой в использовании Objective-C API для работы с файловой системой. Мы получаем ссылку на экземпляр класса, запрашивая у него менеджер по умолчанию.
Менеджеру по умолчанию отправляется сообщение fileExistsAtPath:
и передается путь к файлу, который мы получили в первой строке. Если файл существует в месте, указанном в пути к файлу, мы загружаем содержимое файла в свойство items
. Если в этом месте нет файлов, мы создаем пустой изменяемый массив.
Загрузка файла в свойство items
контроллера представления осуществляется через класс NSKeyedUnarchiver
как мы обсуждали ранее. Класс может читать двоичные данные, содержащиеся в файле, и преобразовывать их в граф объектов, в данном случае это массив экземпляров TSPItem
. Этот процесс станет более понятным, когда мы посмотрим на метод saveItems
через минуту.
Если бы мы загружали содержимое файла, который не существует, то для свойства items
было бы установлено значение nil
вместо пустого изменяемого массива. Это тонкое, но важное отличие, как мы увидим чуть позже в этом уроке.
Давайте теперь посмотрим на вспомогательный метод pathForItems
. Метод начинается с получения пути к каталогу Documents в песочнице приложения. Этот шаг должен быть знакомым.
1
2
3
4
5
6
|
— (NSString *)pathForItems {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documents = [paths lastObject];
return [documents stringByAppendingPathComponent:@»items.plist»];
}
|
Метод возвращает путь к файлу, содержащему список элементов приложения, добавляя строку к пути к каталогу документов. Вы можете прочитать предыдущее предложение несколько раз, чтобы оно впиталось.
Прелесть использования stringByAppendingPathComponent:
в том, что вставка разделителей пути выполняется для нас везде, где это необходимо. Другими словами, система гарантирует, что мы получим правильный путь к файлу.
Сохранение предметов
Несмотря на то, что мы не будем сохранять элементы до тех пор, пока в этом уроке, будет хорошей идеей реализовать его, пока мы на нем. Реализация saveItems
очень лаконична благодаря вспомогательному методу pathForItems
.
Сначала мы получаем путь к файлу, который содержит список элементов приложения, а затем записываем содержимое свойства items
в это место. Легко. Правильно?
1
2
3
4
|
— (void)saveItems {
NSString *filePath = [self pathForItems];
[NSKeyedArchiver archiveRootObject:self.items toFile:filePath];
}
|
Как мы видели ранее, процесс записи графа объектов на диск известен как архивация. Для этого мы используем класс NSKeyedArchiver
, вызывая archiveRootObject:toFile:
NSKeyedArchiver
класса NSKeyedArchiver
.
Во время этого процесса каждому объекту в графе объектов отправляется сообщение encodeWithCoder:
чтобы преобразовать его в двоичные данные. Как я уже говорил ранее, вы обычно не вызываете encodeWithCoder:
напрямую.
Чтобы убедиться, что загрузка списка элементов с диска работает, поместите инструкцию log в метод TSPListViewController
класса TSPListViewController
как показано ниже. Запустите приложение в iOS Simulator и проверьте, все ли работает.
1
2
3
4
5
|
— (void)viewDidLoad {
[super viewDidLoad];
NSLog(@»Items > %@», self.items);
}
|
Если вы посмотрите на вывод в окне консоли, вы заметите, что свойство items
равно en empty array, как мы и ожидали в этот момент. Важно, что items
не равны nil
. На следующем шаге мы дадим пользователю несколько пунктов для работы, процесс, известный как заполнение .
6. Заполнение хранилища данных
Заполнение приложения данными часто может означать разницу между занятым пользователем и пользователем, выходящим из приложения после его использования в течение менее минуты. Заполнение приложения фиктивными данными не только помогает пользователям быстрее освоиться, но и показывает новым пользователям, как приложение выглядит и чувствует себя с данными в нем.
Заполнить приложение списка покупок начальным списком товаров не сложно. Когда приложение запускается, мы сначала проверяем, было ли хранилище данных уже заполнено данными, потому что мы не хотим создавать повторяющиеся элементы. Это только смущает или расстраивает пользователя. Если хранилище данных еще не было заполнено, мы загружаем список с начальными данными и используем этот список для создания хранилища данных приложения.
Логика для заполнения хранилища данных может быть вызвана из нескольких мест в приложении, но важно подумать заранее. Мы могли бы поместить логику для TSPListViewController
хранилища данных в класс TSPListViewController
, но что если в будущей версии приложения другие контроллеры представления также будут иметь доступ к списку элементов. Хорошее место для TSPAppDelegate
хранилища данных — класс TSPAppDelegate
. Посмотрим, как это работает.
Откройте TSPAppDelegate.m и измените реализацию application:didFinishLaunchingWithOptions:
так, как показано ниже.
1
2
3
4
5
6
|
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Seed Items
[self seedItems];
return YES;
}
|
Единственное отличие от предыдущей реализации заключается в том, что мы сначала вызываем seedItems
для делегата приложения. Важно, чтобы заполнение хранилища данных имело место до инициализации любого из контроллеров представления, потому что хранилище данных должно быть заполнено прежде, чем любой из контроллеров представления загрузит список элементов.
Реализация seedItems
не сложна. Мы начинаем с сохранения ссылки на общий пользовательский объект по умолчанию и затем проверяем, есть ли в базе данных пользовательских настроек по умолчанию запись для ключа с именем TSPUserDefaultsSeedItems
и является ли эта запись логическим значением со значением YES
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
— (void)seedItems {
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
if (![ud boolForKey:@»TSPUserDefaultsSeedItems»]) {
// Load Seed Items
NSString *filePath = [[NSBundle mainBundle] pathForResource:@»seed» ofType:@»plist»];
NSArray *seedItems = [NSArray arrayWithContentsOfFile:filePath];
// Items
NSMutableArray *items = [NSMutableArray array];
// Create List of Items
for (int i = 0; i < [seedItems count]; i++) {
NSDictionary *seedItem = [seedItems objectAtIndex:i];
// Create Item
TSPItem *item = [TSPItem createItemWithName:[seedItem objectForKey:@»name»] andPrice:[[seedItem objectForKey:@»price»] floatValue]];
// Add Item to Items
[items addObject:item];
}
// Items Path
NSString *itemsPath = [[self documentsDirectory] stringByAppendingPathComponent:@»items.plist»];
// Write to File
if ([NSKeyedArchiver archiveRootObject:items toFile:itemsPath]) {
[ud setBool:YES forKey:@»TSPUserDefaultsSeedItems»];
}
}
}
|
Ключ может быть любым, если вы последовательны в названии ключей, которые вы используете. Ключ в базе данных пользователя по умолчанию сообщает нам, было ли приложение уже заполнено данными или нет. Это важно, поскольку мы хотим заполнить приложение списка покупок только один раз.
Если приложение еще не было заполнено, мы загружаем список свойств из пакета приложения с именем seed.plist . Этот файл содержит массив словарей, каждый из которых представляет элемент с названием и ценой.
Перед seedItems
массива seedItems
мы создадим изменяемый массив для хранения экземпляров TSPItem
которые мы собираемся создать. Для каждого словаря в массиве seedItems
мы создаем экземпляр TSPItem
, используя метод класса, который мы объявили ранее в этом уроке, и добавляем экземпляр в массив items
.
Наконец, мы создаем путь к файлу, в котором мы будем хранить список элементов, и записываем содержимое массива items
на диск, как мы видели в методе TSPListViewController
класса TSPListViewController
.
Метод archiveRootObject:toFile:
возвращает YES
если операция завершилась успешно, и только тогда мы обновляем хранилище пользовательских настроек по умолчанию, устанавливая логическое значение для ключа TSPUserDefaultsSeedItems
в YES
. В следующий раз, когда приложение запустится, хранилище данных больше не будет заполнено.
Вы, наверное, заметили, что мы использовали другой вспомогательный метод для извлечения каталога документов приложения. Вы можете найти его реализацию ниже. Это очень похоже на реализацию метода TSPListViewController
классе TSPListViewController
.
1
2
3
4
|
— (NSString *)documentsDirectory {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
return [paths lastObject];
}
|
Перед запуском приложения импортируйте файл TSPItem
класса TSPItem
и обязательно скопируйте список свойств seed.plist в ваш проект. Неважно, где вы храните его, если он включен в комплект приложения.
1
|
#import «TSPItem.h»
|
Запустите приложение еще раз и проверьте вывод в консоли, чтобы увидеть, было ли хранилище данных успешно заполнено содержимым seed.plist .
Обратите внимание, что заполнение хранилища данных данными или обновление базы данных требует времени. Если операция занимает слишком много времени, система может убить ваше приложение, прежде чем оно сможет завершить запуск. Apple называет это явление сторожевым псом, убивающим ваше приложение.
Вашему приложению дается ограниченное количество времени для запуска. Если не удается запустить в течение этого периода времени, операционная система убивает ваше приложение. Это означает, что вам необходимо тщательно продумать, когда и где вы выполняете определенные операции, такие как заполнение хранилища данных вашего приложения.
7. Отображение списка товаров
Теперь у нас есть список предметов для работы. Отображение элементов в табличном представлении контроллера представления списка не сложно. Посмотрите на реализацию трех методов протокола UITableViewDataSource
, показанных ниже. Их реализации должны быть вам уже знакомы. Не забудьте импортировать заголовочный файл класса TSPItem
.
1
|
#import «TSPItem.h»
|
1
2
3
|
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
|
1
2
3
|
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.items 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.items objectAtIndex:[indexPath row]];
// Configure Cell
[cell.textLabel setText:[item name]];
return cell;
}
|
Перед запуском приложения объявите идентификатор повторного использования ячейки сразу после директивы компилятора @implementation
как мы видели ранее в этой серии.
1
2
3
|
@implementation TSPListViewController
static NSString *CellIdentifier = @»Cell Identifier»;
|
И наконец, зарегистрируйте класс UITableViewCell
для идентификатора повторного использования ячейки, который вы просто объявляете в методе viewDidLoad
контроллера представления.
1
2
3
4
5
6
7
8
|
— (void)viewDidLoad {
[super viewDidLoad];
// NSLog(@»Items > %@», self.items);
// Register Class for Cell Reuse
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
}
|
Создайте проект и запустите приложение в iOS Simulator, чтобы увидеть список элементов, отображаемых в виде таблицы контроллера списка.
8. Добавление предметов — часть 1
Независимо от того, насколько хорошо мы создадим список начальных элементов, пользователь наверняка захочет добавить дополнительные элементы в список. Наиболее распространенный подход на iOS для добавления новых элементов в список — это предоставление пользователю модального представления, в которое можно вводить новые данные.
Это означает, что нам нужно:
- добавить кнопку для добавления новых предметов
- создать контроллер представления, который управляет представлением, принимающим пользовательский ввод
- создать новый элемент на основе ввода пользователя
- добавить вновь созданный элемент в табличное представление
Добавление кнопки
Добавление кнопки на панель навигации требует одной строки кода. viewDidLoad
метод TSPListViewController
класса TSPListViewController
и обновите его, чтобы отразить реализацию ниже.
01
02
03
04
05
06
07
08
09
10
11
|
— (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:)];
}
|
В уроке о контроллерах панели вкладок мы увидели, что у каждого контроллера представления есть свойство tabBarItem
. Точно так же каждый контроллер представления имеет свойство navigationItem
, уникальный экземпляр UINavigationItem
представляющий контроллер представления на панели навигации родительского контроллера представления — контроллера навигации.
Свойство navigationItem
имеет свойство leftBarButtonItem
, которое является экземпляром UIBarButtonItem
и ссылается на элемент кнопки панели, отображаемый в левой части панели навигации. Свойство navigationItem
также имеет свойство titleView
и свойство rightBarButtonItem
.
В методе viewDidLoad
мы устанавливаем свойство leftBarButtonItem
для navigationItem
элемента UIBarButtonItem
представления UIBarButtonItem
экземпляру UIBarButtonItem
, вызывая initWithBarButtonSystemItem:target:action:
и передавая UIBarButtonSystemItemAdd
в качестве первого аргумента. Этот метод создает системный экземпляр UIBarButtonItem
.
Несмотря на то, что мы уже сталкивались с шаблоном target-action, второй и третий параметр initWithBarButtonSystemItem:target:action:
может потребовать некоторого объяснения. При каждом нажатии кнопки на панели навигации сообщение addItem:
отправляется target
, self
или экземпляру TSPListViewController
.
Как я уже сказал, мы уже сталкивались с шаблоном целевого действия, когда мы связали событие касания кнопки с действием в раскадровке несколько уроков назад. Это очень похоже, единственное отличие состоит в том, что соединение устанавливается программно.
Шаблон целевого действия очень распространен в Какао. Идея проста. Объект сохраняет ссылку на селектор сообщения или действия, который необходимо отправить конкретному объекту и цели, объекту, которому необходимо отправить сообщение.
Подожди минуту. Что такое селектор? Селектор — это имя или уникальный идентификатор, который используется для выбора метода, который должен выполнить объект. Селектор имеет тип SEL
и может быть создан с помощью директивы компилятора @selector
. Вы можете прочитать больше о селекторах в руководстве Apple по компетенциям Cocoa Core .
Перед запуском приложения в iOS Simulator нам нужно создать соответствующий метод addItem:
в контроллере представления. Если мы этого не сделаем, контроллер представления не сможет ответить на сообщение, которое он получает, когда нажимается кнопка, и возникает исключение.
Посмотрите на формат определения метода в фрагменте кода ниже. Как мы видели ранее в этой серии, действие принимает один аргумент — объект, который отправляет сообщение контроллеру представления (цели), который является кнопкой на панели навигации в нашем примере.
1
2
3
|
— (void)addItem:(id)sender {
NSLog(@»Button was tapped.»);
}
|
Я добавил оператор log в реализацию метода, чтобы проверить, правильно ли работает установка. Создайте проект и запустите приложение, чтобы проверить кнопку на панели навигации.
Создание View Controller
Создайте новый подкласс UIViewController
(не подкласс UITableViewController
) и назовите его TSPAddItemViewController
.
Сначала нам нужно объявить два выхода в заголовочном файле класса для двух текстовых полей, которые мы создадим через несколько минут. Этот процесс уже должен быть знаком. Взгляните на файл заголовка, показанный ниже (комментарии опущены).
1
2
3
4
5
6
7
8
|
#import <UIKit/UIKit.h>
@interface TSPAddItemViewController : UIViewController
@property IBOutlet UITextField *nameTextField;
@property IBOutlet UITextField *priceTextField;
@end
|
Нам также нужно объявить два действия в файле реализации класса ( MTAddItemViewController.m ). Первое действие, cancel:
отменит создание нового элемента, тогда как второе действие, save:
сохранит вновь созданный элемент.
1
2
3
|
— (IBAction)cancel:(id)sender {
}
|
1
2
3
|
— (IBAction)save:(id)sender {
}
|
Откройте раскадровку, перетащите экземпляр UIViewController
из библиотеки объектов и установите его класс в TSPAddItemViewController
в инспекторе удостоверений .
Создайте ручной переход, нажав Control
и перетащив из объекта List View Controller в объект Add Item View Controller . Выберите Modal из всплывающего меню.
Выберите только что созданный переход, откройте инспектор атрибутов и установите для его идентификатора AddItemViewController .
Перед тем, как добавить текстовые поля, выберите контроллер представления «Добавить элемент» и вставьте его в контроллер навигации, выбрав « Встроить> Контроллер навигации» в меню « Редактор» .
Ранее в этом уроке мы программно добавили UIBarButtonItem
в элемент навигации контроллера представления списка. Давайте рассмотрим, как это работает в раскадровке. Увеличьте контроллер добавления элемента, добавьте два экземпляра UIBarButtonItem
на его панель навигации, расположив по одному на каждой стороне. Выберите элемент левой панели, откройте инспектор атрибутов и установите для идентификатора значение « Отмена» . Сделайте то же самое для правой кнопки панели, но вместо этого установите Идентификатор на « Сохранить» .
Выберите объект Add View View Controller , откройте Инспектор соединений справа и соедините действие cancel:
с элементом левой кнопки, а действие save:
с элементом правой панели.
Перетащите два экземпляра UITextField
из библиотеки объектов в представление контроллера добавления элемента. Расположите текстовые поля, как показано на скриншоте ниже.
Выберите верхнее текстовое поле, откройте инспектор атрибутов и введите « Имя» в поле « Заполнитель» . Выделите нижнее текстовое поле и в инспекторе атрибутов установите для его текста-заполнителя значение « Цена» и « Клавиатура» — « Цифровая панель» . Это гарантирует, что пользователи могут вводить цифры только в нижнем текстовом поле. Выберите объект Add Item View Controller , откройте инспектор соединений и подключите nameTextField
и priceTextField
к соответствующим текстовым полям в представлении контроллера представления.
Это было довольно много работы. Все, что мы сделали в раскадровке, также может быть выполнено программно. Некоторые разработчики даже не используют раскадровки и программно создают пользовательский интерфейс всего приложения. В любом случае это именно то, что происходит под капотом.
Реализация addItem:
С готовым к использованию классом TSPAddItemViewController
давайте вернемся к addItem:
в классе TSPListViewController
. Однако перед этим импортируем файл TSPAddItemViewController
класса TSPAddItemViewController
вверху.
1
|
#import «TSPAddItemViewController.h»
|
Реализация действия addItem:
коротка, как вы можете видеть ниже. Мы вызываем performSegueWithIdentifier:sender:
и AddItemViewController
идентификатор AddItemViewController
мы установили в раскадровке, и self
, контроллер представления, в качестве второго аргумента.
Мы создаем новый экземпляр класса MTAddItemViewController
и представляем его модально, вызывая presentViewController:animated:completion:
on self
, экземпляр контроллера представления списка. При presentViewController:animated:completion:
addItemViewController
экземпляра addItemViewController
будет перемещаться снизу вверх и будет отображаться в полноэкранном режиме. Метод принимает блок завершения в качестве третьего аргумента. Несмотря на их полезность, я не буду описывать блоки в этом уроке, так как это более сложная тема для начинающих. Вместо того, чтобы передавать блок в качестве третьего аргумента, мы передаем nil
.
1
2
3
4
|
— (void)addItem:(id)sender {
// Perform Segue
[self performSegueWithIdentifier:@»AddItemViewController» sender:self];
}
|
Отклонение Контроллера Представления
Пользователь также должен иметь возможность закрыть контроллер представления, нажав кнопку «Отмена» или «Сохранить» контроллера добавления представления элемента. Пересмотрите действия cancel:
и save:
в классе TSPAddItemViewController
и обновите их реализации, как показано ниже. Мы вернемся к действию save:
чуть позже в этом уроке.
1
2
3
|
— (IBAction)cancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
|
1
2
3
|
— (IBAction)save:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
|
dismissViewControllerAnimated:completion:
интересно. Когда мы вызываем этот метод на контроллере представления, чье представление представлено модально, модальный контроллер представления передает сообщение контроллеру представления, который представил контроллер представления. В нашем примере это означает, что контроллер представления элемента добавления пересылает сообщение в контроллер представления навигации, который, в свою очередь, пересылает его в контроллер представления списка.
Второй аргумент dismissViewControllerAnimated:completion:
является блоком, который выполняется после завершения анимации. Блоки были добавлены в язык C Apple. Их часто сравнивают с замыканиями на других языках. Если вы хотите узнать больше о блоках, посмотрите этот урок от Collin Ruffenach.
Создайте проект и запустите приложение в iOS Simulator, чтобы увидеть класс TSPAddItemViewController
в действии.
8. Добавление предметов — часть 2
Как контроллер представления списка узнает, когда новый элемент был добавлен контроллером представления добавления элемента? Должны ли мы сохранить ссылку на контроллер представления списка, который представил контроллер представления добавления элемента? Это приведет к жесткой связи, что не очень хорошая идея, потому что делает наш код менее независимым и менее пригодным для повторного использования.
Проблема, с которой мы сталкиваемся, может быть решена путем реализации пользовательского протокола делегирования. Посмотрим, как это работает.
Делегация
Идея проста. Когда пользователь нажимает кнопку «Сохранить», контроллер представления «Добавить элемент» будет собирать информацию из текстовых полей и уведомлять своего делегата о том, что новый элемент был сохранен.
Объект делегата будет объектом, соответствующим настраиваемому протоколу делегата, который мы определим. Объект делегата должен решить, что делать с информацией, которую отправляет контроллер представления добавления элемента. Контроллер представления добавления элемента отвечает только за захват ввода пользователя и уведомление его делегата.
Откройте TSPAddItemViewController.h и добавьте объявление прямого протокола вверху. Объявление прямого протокола — это обещание компилятору, что протокол TSPAddItemViewControllerDelegate
определен и существует.
01
02
03
04
05
06
07
08
09
10
|
#import <UIKit/UIKit.h>
@protocol TSPAddItemViewControllerDelegate;
@interface TSPAddItemViewController : UIViewController
@property IBOutlet UITextField *nameTextField;
@property IBOutlet UITextField *priceTextField;
@end
|
Объявите свойство для делегата. Делегат имеет тип id
, что означает, что это может быть любой объект Objective-C. Однако делегат должен соответствовать протоколу TSPAddItemViewControllerDelegate
, как указано именем протокола в угловых скобках после типа свойства. По этой причине мы добавили объявление прямого протокола вверху. weak
спецификатор в объявлении свойства предназначен для управления памятью. Это указывает на то, что ссылка, которую контроллер представления добавления элемента хранит в своем делегате, является слабой ссылкой, а не сильной ссылкой. Хотя управление памятью является важным аспектом разработки Какао, я не буду освещать слабые и сильные ссылки в этом уроке.
01
02
03
04
05
06
07
08
09
10
11
12
|
#import <UIKit/UIKit.h>
@protocol TSPAddItemViewControllerDelegate;
@interface TSPAddItemViewController : UIViewController
@property (weak) id<TSPAddItemViewControllerDelegate> delegate;
@property IBOutlet UITextField *nameTextField;
@property IBOutlet UITextField *priceTextField;
@end
|
Ниже интерфейса класса TSPAddItemViewController
мы объявляем протокол TSPAddItemViewControllerDelegate
. В отличие от протоколов UITableViewDataSource
и UITableViewDelegate
, TSPAddItemViewControllerDelegate
является коротким и простым.
Объявление протокола начинается с @protocol
и заканчивается @end
, аналогично интерфейсу класса, начинающемуся с @interface
и заканчивающему @end
.
Напоминаем, что объявление протокола определяет или объявляет методы, которые должны реализовывать объекты, соответствующие протоколу. Методы в объявлении протокола требуются по умолчанию.Чтобы объявить необязательные методы, @optional
следует использовать директиву компилятора.
Добавляя NSObject
протокол (между угловыми скобками) после имени протокола, мы заявляем, что TSPAddItemViewControllerDelegate
протокол расширяет NSObject
протокол.
TSPAddItemViewControllerDelegate
Протокол определяет только один метод. Метод сообщает объекту делегата, когда новый элемент был добавлен пользователем, и передает имя и цену нового элемента.
1
2
3
|
@protocol TSPAddItemViewControllerDelegate <NSObject> - ( void )controller:( TSPAddItemViewController *)controller didSaveItemWithName 🙁 NSString *)name andPrice 🙁 float )price; @end
|
Как я упоминал в уроке о табличных представлениях , хорошей практикой является передача отправителя сообщения, объекта, уведомляющего объект делегата, в качестве первого аргумента каждого метода делегата. Это облегчает для объекта делегата связь с отправителем в случае необходимости.
Уведомление делегата
Пришло время использовать протокол делегата, который мы объявили минуту назад. Пересмотрите save:
метод в TSPAddItemViewController
классе и обновите его реализацию, как показано ниже.
01
02
03
04
05
06
07
08
09
10
11
|
- ( IBAction )save:( id )sender { // Extract User Input NSString *name = [ self .nameTextField text ]; float price = [[ self .priceTextField text ] floatValue ]; // Notify Delegate
[ self .delegate controller : self didSaveItemWithName :name andPrice :price]; // Dismiss View Controller [ self dismissViewControllerAnimated : YES completion :nil ]; }
|
Содержимое текстовых полей сначала сохраняются в name
и price
переменных, которые затем передаются как аргументы controller:didSaveItemWithName:andPrice:
метода делегата.
Отвечая на события сохранения
Последняя часть головоломки — привести TSPListViewController
класс в соответствие с TSPAddItemViewControllerDelegate
протоколом. Откройте TSPListViewController.h , импортируйте файл заголовка TSPAddItemViewController
класса и обновите объявление интерфейса, TSPListViewController
чтобы класс соответствовал новому протоколу.
1
2
3
4
5
6
7
|
#import <UIKit/UIKit.h>
#import "TSPAddItemViewController.h" @interface TSPListViewController : UITableViewController <TSPAddItemViewControllerDelegate> @end
|
Оператор import требуется, чтобы сообщить компилятору о TSPAddItemViewControllerDelegate
протоколе, который мы объявили в заголовочном файле TSPAddItemViewController
класса. Поскольку мы уже импортируем файл заголовка TSPAddItemViewController
в TSPListViewController.h , вы можете удалить оператор импорта в файле реализации, TSPListViewController.m .
Как мы сделали с UITableViewDataSource
и UITableViewDelegate
протоколами, мы должны реализовать методы , определенные в TSPAddItemViewControllerDelegate
протоколе. В TSPListViewController.m добавьте следующую реализацию controller:didSaveItemWithName:andPrice:
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
- ( void )controller:( TSPAddItemViewController *)controller didSaveItemWithName 🙁 NSString *)name andPrice 🙁 float )price { // Create Item TSPItem *item = [ TSPItem createItemWithName :name andPrice :price]; // Add Item to Data Source [ self .items addObject :item]; // Add Row to Table View NSIndexPath *newIndexPath = [ NSIndexPath indexPathForItem :([ self .items count ] - 1 ) inSection : 0 ]; [ self .tableView insertRowsAtIndexPaths :@[newIndexPath] withRowAnimation :UITableViewRowAnimationNone]; // Save Items [ self saveItems ]; }
|
Мы создаем новый экземпляр TSPItem
класса, вызывая метод класса, который мы создали ранее, и передаем имя и цену, которые мы получаем от экземпляра контроллера представления добавления элемента.
На следующем шаге items
свойство обновляется путем добавления вновь созданного элемента. Конечно, табличное представление не отражает автоматически добавление нового элемента. Нам нужно вручную вставить новую строку в табличное представление. Чтобы сохранить новый элемент на диске, мы вызываем saveItems
контроллер представления, который мы реализовали ранее в этом руководстве.
Установка делегата
Последняя часть этой довольно сложной головоломки — установить делегат контроллера представления добавления элемента, когда мы представляем его пользователю. Мы делаем это, prepareForSegue:sender:
как мы видели ранее в этой серии.
01
02
03
04
05
06
07
08
09
10
11
12
|
— (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 ]; }
}
|
Если идентификатор segue равен AddItemViewController
, мы запрашиваем его у segue destinationViewController
. Вы можете подумать, что контроллер представления назначения является контроллером представления добавления элемента, но помните, что контроллер представления добавления элемента встроен в контроллер навигации.
Другими словами, нам нужно получить первый элемент в стеке навигации контроллера навигации, который дает нам корневой контроллер представления или объект контроллера добавления представления элемента, который мы ищем. Затем мы устанавливаем delegate
свойство контроллера представления добавления элемента равным контроллеру self
представления.
Создайте проект и запустите приложение еще раз, чтобы увидеть, как все работает вместе — как по волшебству.
Вывод
Это было много, чтобы принять, но мы уже сделали немало. На следующем уроке мы внесем некоторые изменения в контроллер представления списка, чтобы редактировать и удалить элементы из списка, а также добавим возможность создавать список покупок из списка элементов.