Использование блоков в iOS. Часть 2
Введение
Блоки были впервые введены в iOS 4.0 и Mac OSX 10.6 для упрощения кода. Они могут помочь в его уменьшении, в снижении зависимости от делегатов, и в написании чистого, читабельного кода. Но, несмотря на очевидные преимущества, многие разработчики не используют блоки ввиду неполного понимания принципа их действия.
Давайте разберем все «что, где, когда и почему» блоков.
Что такое эти «Блоки» и почему они такие важные?
Изнутри блок представляет собой кусок кода, который может быть вызван в некоторый момент в будущем. Блок — это функция первого класса, поэтому можно сказать, что блоки — обычные объекты Objective-C. А объекты можно передавать как параметры, возвращать из функций и методов, и присваивать переменным. Блоки зачастую называют замыканиями (closures) в таких языках, как Python, Ruby и Lisp, потому что после объявления они инкапсулируют свое состояние. Блок создает константную копию любой локальной переменной, на которую ссылается.
Когда вам понадобится вызвать выполнение некоторого кода, который бы позже вам вернул что-либо, вероятно вы бы использовали делегаты или NSNotificationCenter, а не блоки. И это бы прекрасно сработало, но куски кода были бы разбросаны повсюду — задача стартует с одной точки, а результат обрабатывается в другой.
Блоки лучше, потому что они держат выполнение задачи в одном месте, что мы скоро и увидим.
Для кого нужны блоки?
Для ВАС! А если серьезно, блоки — для всех и все используют блоки. Блоки — это будущее, так что вы можете изучить их прямо сейчас. Множество методов встроенных фреймворков переписываются или дополняются блоками на базе существующих функциональных возможностей.
Как использовать блоки?
Это картинка, из iOS Developer Library хоршо объясняет синтаксис блоков.
Формат описания блоков такой:
return_type (^block_name)(param_type, param_type, ...)
Если вы уже программировали на любом другом С-языке, эта конструкция вам вполне знакома за исключением этого символа ^. ^ и означает блок. Если вы поняли, что ^ означает «я — блок», — поздравляю — вы только что поняли самую трудную вещь касаемо блоков. ;]
Обратите внимание, что имена для параметров на данный момент не требуются, но по желанию можете их включить.
Ниже приведен образец объявления блока.
int (^add)(int,int)
Далее, описание блока.
^return_type(param_type param_name, param_type param_name, ...) { ... return return_type; }
Собственно, так блок и создается. Заметьте, что описание блока отличается от его объявления. Оно начинается с символа ^ и сопровождается параметрами, которые могут быть поименованы и которые обязаны соответствовать типу и порядку параметров в объявлении блока. Далее следует сам код.
В определении блока тип возвращаемого значения указывать необязательно, он может быть идентифицирован из кода. Если возвращаемых значений несколько, они обязаны быть одного типа.
Образец описания блока:
^(int number1, int number2){ return number1+number2 }
Если совместить описание и объявление блока, то получится следующий код:
int (^add)(int,int) = ^(int number1, int number2){
return number1+number2;
}
Еще можно использовать блоки таким образом:
int resultFromBlock = add(2,2);
Теперь давайте посмотрим на пару примеров использования блоков в противопоставлении с таким же кодом без блоков.
Пример: NSArray
Давайте посмотрим, как блоки меняют принцип выполнения некоторых операций над массивом. Для начала — вот обычный код для цикла:
BOOL stop;
for (int i = 0 ; i < [theArray count] ; i++) {
NSLog(@"The object at index %d is %@",i,[theArray objectAtIndex:i]);
if (stop)
break;
}
Вы можете не придать значения переменной «stop». Но все станет ясно после того, как вы увидите реализацию этого метода с использованием блока. Способ с блоком содержит переменную «stop», которая позволяет остановить цикл в любой момент, и поэтому мы просто дублируем эту возможность здесь для схожести кода.
Теперь посмотрим на этот же код с использованием быстрого перечисления (fast-enumeration).
BOOL stop;
int idx = 0;
for (id obj in theArray) {
NSLog(@"The object at index %d is %@",idx,obj);
if (stop)
break;
idx++;
}
И теперь с блоком:
[theArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
NSLog(@"The object at index %d is %@",idx,obj);
}];
В коде выше вы можете задаться вопросом — что за переменная «stop». Это просто переменная, которой может быть присвоено YES внутри блока для остановки процесса. Этот параметр определен как часть блока, которая будет использоваться в методе enumerateObjectsUsingBlock.
Вышеуказанный код тривиален, и увидеть в нем преимущества блоков довольно трудно. Но есть две вещи, которые я бы хотел отметить:
- Простота. Используя блоки, мы можем получить доступ к объекту, индексу объекта в массиве и стоп-переменной без написания кода. Это приводит к уменьшению кода, что в свою очередь уменьшает количество ошибок.
- Скорость. Блочный метод имеет небольшое преимущество в скорости по сравнению с методом быстрого перечисления. В нашем случае это преимущество, возможно, слишком мало для упоминания, но в более сложных случаях оно становится существенным.
Пример: UIView-анимации
Давайте рассмотрим простую анимацию, которая оперирует на одном UIView. Она меняет параметр alpha до 0 и перемещает view вниз и вправо на 50 пунктов, затем удаляет UIView из superview. Просто, не так ли? Код без блоков:
- (void)removeAnimationView:(id)sender {
[animatingView removeFromSuperview];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[UIView beginAnimations:@"Example" context:nil];
[UIView setAnimationDuration:5.0];
[UIView setAnimationDidStopSelector:@selector(removeAnimationView)];
[animatingView setAlpha:0];
[animatingView setCenter:CGPointMake(animatingView.center.x+50.0,
animatingView.center.y+50.0)];
[UIView commitAnimations];
}
Блочный метод:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[UIView animateWithDuration:5.0
animations:^{
[animatingView setAlpha:0];
[animatingView setCenter:CGPointMake(animatingView.center.x+50.0,
animatingView.center.y+50.0)];
}
completion:^(BOOL finished) {
[animatingView removeFromSuperview];
}];
}
Если внимательно посмотреть на эти два метода, можно заметить три преимущества блочного метода:
- Упрощает код. С блоком нам не нужно определять абсолютно отдельный метод для завершения обратного вызова, или вызывать beginAnimations/commitAnimations.
- Весь код лежит в одном месте. С использованием блока не нужно начинать анимацию в одном месте, а метод обратного вызова — в другом. Весь код, связанный с анимацией, находится в одном месте, что облегчает его написание и чтение.
- Так говорит Apple. Apple переписали существующий функционал, не использовавший блоки, так что Apple официально рекомендует переходить к методам на основе блоков, если возможно.
Когда использовать блоки?
Я считаю, лучший совет — использовать блоки там, где они нужны. Возможно, вы хотите продолжить использовать старые методы, чтобы сохранить обратную совместимость, или просто потому, что больше больше с ними знакомы. Но каждый раз, когда вы подходите к подобной точке принятия решения, подумайте, смогут ли блоки облегчить вам жизнь написание кода, и стоит ли использовать методы с блочным подходом.
Конечно, со временем вы возможно заметите, что стали все больше нуждаться в блоках просто потому, что большинство фреймворков написаны и переписаны с использованием блоков. Поэтому начните использовать блоки сейчас, чтобы встретиться с ними в будущем во всеоружии.
Возвращаясь к iOS Diner: настраиваем классы модели
Продолжим с того же места, на котором остановились в первой части. Если вы не читали первую часть, или просто хотите освежить все в памяти, можете скачать проект здесь.
Откройте проект в Xcode и переключитесь в Project Navigator. Щелкните правой кнопкой мыши по папке iOSDiner и выберите New Group. Назовем ее «Models».
Теперь правой кнопкой мыши кликаем по созданной папке Models и выбираем New File → Objective-C класс (или Cocoa Touch Class). Назовем его «IODItem» и в качестве родительского класса выберем NSObject.
Повторите вышеуказанные действия, чтобы создать класс IODOrder.
Теперь у нас есть все необходимые классы. Настало время кода.
Настройка IODItem
Откройте IODItem.h. Прежде всего, в класс нужно добавить протокол NSCopying. Протоколы
являются неким «соглашением» о методах, которые класс будет реализовывать. Если в классе объявляется протокол, тогда в этом же классе необходимо определить обязательные, или опциональные методы, которые описаны в протоколе. Объявление протокола выглядит следующим образом:
@interface IODItem : NSObject
Далее, добавим свойства для объекта. У каждого объекта будет имя, цена и имя файла с изображением. IODItem.h будет выглядеть так:
#import
@interface IODItem : NSObject
@property (nonatomic,strong) NSString* name;
@property (nonatomic,strong) float price;
@property (nonatomic,strong) NSString* pictureFile;
@end
Теперь переключимся на IODItem.m. Тут мы видим предупреждение.
Это предупреждение относится к протоколу NSCopying, который мы добавили ранее. Помните, что протоколы могут описывать обязательные методы? Вот и NSCopying требует определить — (id)copyWithZone:(NSZone *)zone
. Пока этот метод не будет добавлен, класс будет считаться незаконченным. Добавьте в IODItem.m перед @end следующее:
- (id)copyWithZone:(NSZone *)zone {
IODItem *newItem = [[IODItem alloc] init];
newItem.name = _name;
newItem.price = _price;
newItem.pictureFile = _pictureFile;
return newItem;
}
Вот и все, предупреждение исчезло. Этот код создает новый IODItem-объект, копирует свойства уже существующего объекта, и возвращает этот новый экземпляр.
Дополнительно необходим метод-инициализатор. Он позволяет легко и быстро выставить значения свойств по умолчанию, когда инициализируется объект. Напишите в IODItem.m:
- (id)initWithName:(NSString *)name
andPrice:(NSNumber *)price
andPictureFile:(NSString *)pictureFile {
if(self = [self init])
{
_name = name;
_price = price;
_pictureFile = pictureFile;
}
return self;
}
Вернитесь в IODItem.h и добавьте прототип метода перед окончанием файла (@end):
- (id)initWithName:(NSString*)inName andPrice:(float)inPrice andPictureFile:(NSString*)inPictureFile;
Настройка IODOrder
Далее, поработаем над другим классом — IODOrder. Этот класс будет представлять собой заказ и операции, с ним связанными, — добавление объекта, удаление, вычисление полной стоимости заказа и вывода содержания заказа на экран.
Откройте IODOrder.h и перед interface добавьте импорт заголовочного файла:
#import "IODItem.h"
А под interface пропишем свойство:
@property (nonatomic,strong) NSMutableDictionary* orderItems;
Это словарь, в котором будут храниться объекты, выбранные пользователем.
Настройка ViewController
Переключимся в ViewController.m, добавим свойства:
#import "ViewController.h"
#import "IODOrder.h"
@interface ViewController ()
@property (assign, nonatomic) NSInteger currentItemIndex;
@property (strong, nonatomic) NSMutableArray *inventory;
@property (strong, nonatomic) IODOrder *order;
Свойство currentItemIndex будет запоминать индекс просматриваемого объекта в инвентаре (inventory). Смысл inventory легко объяснить — это массив IODItem-элементов, которые мы получим из веб-сервиса. order — объект класса IODOrder, который хранит объекты, выбранные пользователем.
Далее в методе viewDidLoad нужно обнулить currentItemIndex и order.
- (void)viewDidLoad {
[super viewDidLoad];
self.currentItemIndex = 0;
self.order = [[IODOrder alloc] init];
}
Теперь начало ViewController.m выглядит так:
#import "ViewController.h"
#import "IODOrder.h"
@interface ViewController ()
@property (assign, nonatomic) NSInteger currentItemIndex;
@property (strong, nonatomic) NSMutableArray *inventory;
@property (strong, nonatomic) IODOrder *order;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.currentItemIndex = 0;
self.order = [[IODOrder alloc] init];
}
Постройте проект. Никаких предупреждений быть не должно.
Загружаем инвентарь
Метод класса retrieveInventoryItems, который мы сейчас добавим, будет загружать и обрабатывать инвентарь, загруженный с веб-сервиса.
Примечание. Методы класса начинаются с »+», а методы экземпляра класса — с »-».
В IODItem.m сразу после директив #import добавим следующее:
#define INVENTORY_ADDRESS @"http://adamburkepile.com/inventory/"
Примечание. Измените адрес, если веб-сервис вы размещали самостоятельно.
Далее добавим метод retrieveInventoryItems.
+ (NSArray *)retrieveInventoryItems {
// 1 — создаем переменные
NSMutableArray *inventory = [[NSMutableArray alloc] init];
NSError *error = nil;
// 2 — получаем данные об инвентаре
NSArray *jsonInventory = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:INVENTORY_ADDRESS]]
options:kNilOptions
error:&error];
// 3 — смотрим все объекты инвентаря
[jsonInventory enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSDictionary *item = obj;
[inventory addObject:[[IODItem alloc] initWithName:item[@"Name"]
andPrice:item[@"Price"]
andPictureFile:item[@"Image"]]];
}];
// 4 — возвращаем копию инвентаря
return [inventory copy];
}
А вот и наш первый блок. Давайте подробнее пройдемся по коду.
- Во-первых, мы объявили массив, в котором будем хранить объекты для возврата, и указатель на возможную ошибку.
- Мы используем объект NSData для загрузки данных с веб-сервера, и затем передаем этот NSData-объект в JSON-сервис, чтобы декодировать исходные данные в Objective-C типы (NSArray, NSDictionary, NSString, NSNumber, и т. д.).
- Далее нам необходим enumerateObjectsUsingBlock: метод, которые мы обсуждали ранее, для «конвертирования» объектов из NSDictionary в IODItem. Мы вызываем этот метод для массива jsonInventory, и перебираем его с помощью блока, который передает элемент массива как NSDictionary в метод инициализации IODItem-объекта. Затем он добавляет этот новый объект в возвращаемый массив.
- В заключение, возвращается массив инвентаря inventory. Заметьте, что возвращаем копию массива, а не сам массив, потому что мы не хотим возвращать его изменяемую версию. Метод copy создает неизменяемую копию.
Теперь откроем IODItem.h и добавим прототип метода:
+ (NSArray*)retrieveInventoryItems;
Dispatch Queue и Grand Central Dispatch
Еще одна вещь, которую обязательно нужно знать, — это dispatch queue (очереди). Переключимся в ViewController.m и добавим свойство.
@property (strong, nonatomic) dispatch_queue_t queue;
В конце метода ViewDidLoad пропишем строку:
self.queue = dispatch_queue_create("com.adamburkepile.queue",nil);
Первый параметр метода dispatch_queue_create — это имя очереди. Можете назвать ее, как хотите, но это имя должно быть уникальным. Поэтому Apple рекомендует для этого имени обратный DNS-стиль.
Теперь давайте используем эту очередь. Пропишем во viewDidAppear:
self.ibChalkboardLabel.text = @"Loading inventory...";
self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
self.ibChalkboardLabel.text = @"Inventory Loaded\n\nHow can I help you?";
Запустите проект. Что-то не так, верно? Мы используем retrieveInventoryItems метод, который определили в IODItem для вызова веб-сервиса, возвращаем объекты инвентаря и помещаем их в массив.
Помните пятисекундную задержку в PHP-скрипте из прошлой части? Но, когда мы запускаем программу, мы не видим «Loading inventory…», затем ждем пять секунд, и видим «Inventory Loaded».
Проблема вот в чем: вызов веб-сервиса блокирует и «замораживает» главную очередь, и не позволяет ей изменить текст лейбла. Если бы только у нас была отдельная очередь, которую можно было бы использовать для подобных операций, не обращаяся к главной очереди… Стоп! У нас она уже есть. Вот где Grand Central Dispatch и блоки могут нам помочь довольно легко. С Grand Central Dispatch мы можем поместить обработку данных в другую очередь, не блокируя главную. Заменим две последние строки во viewDidAppear на следующее:
dispatch_async(self.queue, ^{
self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
dispatch_async(dispatch_get_main_queue(), ^{
self.ibChalkboardLabel.text = @"Inventory Loaded\n\nHow can I help you?";
});
});
Заметьте, что здесь мы используем два разных блока, которые ничего не возвращают и не имеют параметров.
Снова запустите проект. Вот теперь работает, как надо.
Вы не задавались вопросом, зачем мы вызываем dispatch_async для присвоения текста лейблу? Когда вы ставите текст в лейбл, вы обновляете элемент UI, а все, что относится к UI, должно обрабатываться в главной очереди. Поэтому мы еще раз вызываем dispatch_async, но только берем главную очередь и выполняем блок в ней.
Данный метод довольно распространен, когда операция занимает много времени и необходимо обновление UI-элементов.
Grand Central Dispatch — довольно сложная система, роль которой невозможно понять в этом простом уроке. Если вы заинтересованы, почитайте Multithreading and Grand Central Dispatch on iOS for Beginners.
Дополнительные методы
Мы используем веб-сервис для загрузки и хранения инвентаря. Теперь нам нужны три метода-помощника, которые будут отображать хранимый инвентарь пользователю.
Первый метод, findKeyForOrderItem: , мы добавим в IODOrder.m. Он будет полезен для доступа к объекту из словаря.
- (IODItem *)findKeyForOrderItem:(IODItem *)searchItem {
//1 - находим соответствующий индекс объекта
NSIndexSet *indexes = [[self.orderItems allKeys] indexesOfObjectsPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
IODItem *key = obj;
return ([searchItem.name isEqualToString:key.name] && searchItem.price == key.price);
}];
//2 - возвращаем первый совпавший объект
if([indexes count] >=1) {
IODItem *key = [self.orderItems allKeys][[indexes firstIndex]];
return key;
}
//3 - если ничего не нашли
return nil;
}
Давайте разберемся, что этот метод делает. Но прежде чем сделать это, я должен объяснить, почему он вообще нужен. Объект IODOrder содержит словарь orderItems (пары ключ-значение). Ключом будет IODItem, а значением — NSNumber, которое показывает, сколько таких IODItem было заказано.
В теории все хорошо, но своеобразной причудой класса NSDictionary является то, что когда вы хотите назначить объект в качестве ключа, он не присваивает этот объект, а создает его копию и использует в качестве ключа. Это означает, что объект, который вы используете как ключ, должен соответствовать протоколу NSCopying (вот поэтому мы в IODItem объявляли протокол NSCopying).
Ключ в словаре orderItems и IODItem в массиве inventory не являются одним и тем же объектом (хотя и имеют одинаковые свойства), поэтому простой поиск по ключу мы сделать не можем. Вместо этого нам придется сравнивать имя и цену каждого объекта, чтобы определить их совпадение. Это и делает вышеуказанный метод: сравнивает свойства ключей, чтобы найти нужный.
А вот что делает код:
- Мы перебираем ключи словаря orderItems, используя метод indexesOfObjectsPassingTest: для нахождения совпадений имени и цены, — еще один пример блока. Обратите внимание на BOOL после ^. Это тип возвращаемого значения. Данный метод обрабатывает массив, используя блок для сравнения двух объектов, возвращает индексы всех объектов, которые проходят тест, описанный в блоке.
- Здесь мы берем первый индекс из возвращенных
- 3. Возвращаем nil, если совпадающий ключ не был найден.
Не забудьте добавить прототип метода в IODOrder.h.
- (IODItem*)findKeyForOrderItem:(IODItem*)searchItem;
Переходм в ViewController.m и добавляем следующий метод:
- (void)updateCurrentInventoryItem {
if (self.currentItemIndex >=0 && self.currentItemIndex < [self.inventory count]) {
IODItem* currentItem = self.inventory[self.currentItemIndex];
self.ibCurrentItemLabel.text = currentItem.name;
self.ibCurrentItemLabel.adjustsFontSizeToFitWidth = YES;
self.ibCurrentItemImageView.image = [UIImage imageNamed:[currentItem pictureFile]];
}
}
Используя currentItemIndex и массив inventory, этот метод отображает имя и картинку для каждого объекта инвентаря.
Прописываем еще один метод:
- (void)updateInventoryButtons {
if (!self.inventory || ![self.inventory count]) {
self.ibAddItemButton.enabled = NO;
self.ibRemoveItemButton.enabled = NO;
self.ibNextItemButton.enabled = NO;
self.ibPreviousItemButton.enabled = NO;
self.ibTotalOrderButton.enabled = NO;
} else {
if (self.currentItemIndex <= 0) {
self.ibPreviousItemButton.enabled = NO;
} else {
self.ibPreviousItemButton.enabled = YES;
}
if (self.currentItemIndex >= [self.inventory count]-1) {
self.ibNextItemButton.enabled = NO;
} else {
self.ibNextItemButton.enabled = YES;
}
IODItem* currentItem = self.inventory[self.currentItemIndex];
if (currentItem) {
self.ibAddItemButton.enabled = YES;
} else {
self.ibAddItemButton.enabled = NO;
}
if (![self.order findKeyForOrderItem:currentItem]) {
self.ibRemoveItemButton.enabled = NO;
} else {
self.ibRemoveItemButton.enabled = YES;
}
if (![self.order.orderItems count]) {
self.ibTotalOrderButton.enabled = NO;
} else {
self.ibTotalOrderButton.enabled = YES;
}
}
}
Этот метод — самый длинный метод из трех методов-помощников, но довольно простой. Он проверяет состояния программы и определяет, делать ли активной ту или иную кнопку.
Например, если currentItemIndex равен нулю, ibPreviousItemButton не должна быть активной, потому как нет элементов раньше первого. Если в orderItems нет элементов, значит и кнопка ibTotalOrderButton не должна быть активной, потому что еще нет заказа, для которого можно было бы посчитать сумму.
Итак, с этими тремя методами теперь можно творить магию. Вернемся в viewDidAppear во ViewController.m и добавим в самом начале:
[self updateInventoryButtons];
Затем, замените блок на этот:
dispatch_async(queue, ^{
self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateInventoryButtons];
[self updateCurrentInventoryItem];
self.ibChalkboardLabel.text = @"Inventory Loaded\n\nHow can I help you?";
});
});
Постройте и запустите.
О, а вот и гамбургер. Но мы хотим увидеть и остальную еду, так что давайте заставим кнопки работать.
Методы ibaLoadNextItem: и ibaLoadPreviousItem: у нас уже есть вo ViewController.m. Добавим немного кода.
- (IBAction)ibaLoadPreviousItem:(UIButton *)sender {
self.currentItemIndex--;
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
}
- (IBAction)ibaLoadNextItem:(UIButton *)sender {
self.currentItemIndex++;
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
}
Благодаря тем методам-помощникам переключение между объектами с изменением currentItemIndex и обновлением UI стало очень простым. Скомпилируйте и запустите. Теперь можно увидеть все меню.
Делаем удаление и добавление объекта
У нас есть меню, но нет официанта, чтобы принимать заказ. Другими словами, кнопки добавления и удаления не работают. Что ж, время настало.
Нужен еще один метод-помощник в классе IODOrder. В IODOrder.m пишем следующее:
- (NSMutableDictionary *)orderItems{
if (!_orderItems) {
_orderItems = [[NSMutableDictionary alloc] init];
}
return _orderItems;
}
Это простой getter-метод для свойства orderItems. Если в orderItems уже что-то есть, метод вернет объект. А если нет — создаст новый словарь и присвоит его orderItems, а потом вернет.
Далее поработаем над методом orderDescription. Этот метод даст нам строк для вывода на меловую доску. В IODOrder.m пишем:
- (NSString*)orderDescription {
// 1 - Создаем строку
NSMutableString* orderDescription = [[NSMutableString alloc] init];
// 2 - Сортируем объекты по имени
NSArray* keys = [[self.orderItems allKeys] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
IODItem* item1 = (IODItem*)obj1;
IODItem* item2 = (IODItem*)obj2;
return [item1.name compare:item2.name];
}];
// 3 - перебираем объекты и добавляем имя и количество в строку
[keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
IODItem* item = (IODItem*)obj;
NSNumber* quantity = (NSNumber*)[self.orderItems objectForKey:item];
[orderDescription appendFormat:@"%@ x%@\n", item.name, quantity];
}];
// 4 - возвращаем строку с описанием заказа
return [orderDescription copy];
}
Немного пояснений:
- Это строка для описания заказа. Каждый объект заказа будет в нее добавляться.
- Этот кусочек кода берет массив из ключей словаря orderItems и использует блок sortedArrayUsingComparator: для сортировки ключей по имени.
- Этот код использует уже отсортированный массив ключей и вызывает уже знакомый enumerateObjectsUsingBlock: . Каждый ключ преобразуем в IODItem, получаем значение (количество), и добавляем строку в orderDescription.
- Наконец, возвращаем строку orderDescription, но снова только ее копию.
Идем в IODOrder.h и добавляем прототипы этих двух методов.
- (void)updateCurrentInventoryItem;
- (void)updateInventoryButtons;
Теперь, когда мы можем получить строку описания заказа из объекта, переключаемся в ViewController.m и добавляем метод для вызова.
- (void)updateOrderBoard {
if (![self.order.orderItems count]) {
self.ibChalkboardLabel.text = @"No Items. Please order something!";
} else {
self.ibChalkboardLabel.text = [self.order orderDescription];
}
}
Этот метод проверяет количество объектов в заказе. Если их нет, возвращается строка «No Items. Please order something!». Если иначе, метод использует orderDescription метод из класса IODOrder для отображения строки с описанием заказа.
Теперь мы можем обновлять доску в соответствии с текущим заказом. Обновляем наш блок из viewDidAppear:
dispatch_async(self.queue, ^{
self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateOrderBoard]; //<----Добавить эту строку
[self updateInventoryButtons];
[self updateCurrentInventoryItem];
self.ibChalkboardLabel.text = @"Inventory Loaded\n\nHow can I help you?";
});
});
Я понимаю, что это может казаться бессмысленным, так как пару строчек ниже мы просто переписали исходный текст, но ради аккуратности это можно сделать.
Следующий метод будет добавлять объект в заказ. Идем в IODOrder.m.
- (void)addItemToOrder:(IODItem*)inItem {
// 1 - ищем объект в заказе
IODItem* key = [self findKeyForOrderItem:inItem];
// 2 - если объект не существует - добавляем
if (!key) {
[self.orderItems setObject:[NSNumber numberWithInt:1] forKey:inItem];
} else {
// 3 - если существует - обновляем количество
NSNumber* quantity = self.orderItems[key];
int intQuantity = [quantity intValue];
intQuantity++;
// 4 - Обновляем заказ с новым количеством
[self.orderItems removeObjectForKey:key];
[self.orderItems setObject:[NSNumber numberWithInt:intQuantity] forKey:key];
}
}
Поясняю:
- Используем ранее добавленный метод для поиска ключа для orderItem. Не забудьте, что, если объект не найден, он просто вернет nil.
- Если объект не был найден в заказе, добавляем его в количестве, равным 1.
- Если же объект был найден, смотрим на его количество и увеличиваем его на 1.
- Наконец, удаляем исходную запись и записываем новую версию с обновленным количеством.
Метод removeItemFromOrder: почти такой же, как и addItemToOrder: . В IODOrder.m пишем:
- (void)removeItemFromOrder:(IODItem*)inItem {
// 1 - ищем объект в заказе
IODItem* key = [self findKeyForOrderItem:inItem];
// 2 - удаляем объект, только если он существует
if (key) {
// 3 - берем количество, и уменьшаем на единицу
NSNumber* quantity = self.orderItems[key];
int intQuantity = [quantity intValue];
intQuantity--;
// 4 - удаляем объект
[[self orderItems] removeObjectForKey:key];
// 5 - добавляем новый объект с обновленным количеством только если количество больше 0
if (intQuantity)
[[self orderItems] setObject:[NSNumber numberWithInt:intQuantity] forKey:key];
}
}
Заметьте, что при удалении объекта из заказа нам нужно что-то делать, только если объект найден в заказе. Если он найден, мы смотрим его количество, уменьшаем на 1, удаляем объект из словаря и вставляем туда новый объект, если количество больше 0.
Идем в IODOrder.h и добавляем прототипы:
- (void)addItemToOrder:(IODItem*)inItem;
- (void)removeItemFromOrder:(IODItem*)inItem;
Теперь во ViewController.m можно прописать код для кнопок удаления и добавления объектов:
- (IBAction)ibaRemoveItem:(UIButton *)sender {
IODItem* currentItem = self.inventory[self.currentItemIndex];
[self.order removeItemFromOrder:currentItem];
[self updateOrderBoard];
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
}
- (IBAction)ibaAddItem:(UIButton *)sender {
IODItem* currentItem = self.inventory[self.currentItemIndex];
[self.order addItemToOrder:currentItem];
[self updateOrderBoard];
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
}
Все, что нам нужно сделать в обоих методах — получить текущий объект из массива инвентаря, передать его в addItemToOrder: или removeItemFromOrder: , и обновить UI.
Соберите и постройте проект. Теперь объекты можно добавлять и удалять, и доска обновляется.
UIAnimation
Давайте немного оживим нашу программу. Измените ibaRemoveItem: и ibaAddItemMethod:
- (IBAction)ibaRemoveItem:(UIButton *)sender {
IODItem* currentItem = [self.inventory objectAtIndex:self.currentItemIndex];
[self.order removeItemFromOrder:currentItem];
[self updateOrderBoard];
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
UILabel* removeItemDisplay = [[UILabel alloc] initWithFrame:self.ibCurrentItemImageView.frame];
removeItemDisplay.center = self.ibChalkboardLabel.center;
removeItemDisplay.text = @"-1";
removeItemDisplay.textAlignment = NSTextAlignmentCenter;
removeItemDisplay.textColor = [UIColor redColor];
removeItemDisplay.backgroundColor = [UIColor clearColor];
removeItemDisplay.font = [UIFont boldSystemFontOfSize:32.0];
[[self view] addSubview:removeItemDisplay];
[UIView animateWithDuration:1.0
animations:^{
removeItemDisplay.center = [self.ibCurrentItemImageView center];
removeItemDisplay.alpha = 0.0;
}
completion:^(BOOL finished) {
[removeItemDisplay removeFromSuperview];
}];
}
- (IBAction)ibaAddItem:(UIButton *)sender {
IODItem* currentItem = [self.inventory objectAtIndex:self.currentItemIndex];
[self.order addItemToOrder:currentItem];
[self updateOrderBoard];
[self updateCurrentInventoryItem];
[self updateInventoryButtons];
UILabel* addItemDisplay = [[UILabel alloc] initWithFrame:self.ibCurrentItemImageView.frame];
addItemDisplay.text = @"+1";
addItemDisplay.textColor = [UIColor whiteColor];
addItemDisplay.backgroundColor = [UIColor clearColor];
addItemDisplay.textAlignment = NSTextAlignmentCenter;
addItemDisplay.font = [UIFont boldSystemFontOfSize:32.0];
[[self view] addSubview:addItemDisplay];
[UIView animateWithDuration:1.0
animations:^{
[addItemDisplay setCenter:self.ibChalkboardLabel.center];
[addItemDisplay setAlpha:0.0];
}
completion:^(BOOL finished) {
[addItemDisplay removeFromSuperview];
}];
}
Кода много, но он простой. Первая часть создает UILabel и выставляет его свойства. Вторая — анимация, которая перемещает созданный лейбл. Это и есть пример UIView-анимации с блоком, который мы описывали в начале урока.
Запустите проект. Теперь можно увидеть анимацию при нажатии на кнопки добавления и удаления объекта.
Получение суммы (Total)
Последний метод, который нам потребуется добавить в IODOrder.m — метод подсчета суммы заказа.
- (float)totalOrder {
// 1 - Объявляем и инициализируем переменную для суммы
__block float total = 0.0;
// 2 - Блок для полсчета
float (^itemTotal)(float,int) = ^float(float price, int quantity) {
return price * quantity;
};
// 3 - Перебираем объекты и суммируем стоимость каждого
[self.orderItems enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
IODItem* item = (IODItem*)key;
NSNumber* quantity = (NSNumber*)obj;
int intQuantity = [quantity intValue];
total += itemTotal([item.price floatValue], intQuantity);
}];
// 4 - Возвращаем сумму
return total;
}
Пройдемся по коду:
- Объявляем и инициализируем переменную, которая будет хранить сумму. Обратите внимание на __block. Мы будем использовать эту переменную внутри блока. Если мы не напишем __block, блок, который мы создали ниже, создаст константную копию этой переменной и использует ее внутри себя. Это означает, что мы не сможем изменить ее внутри блока. Путем добавления этого ключевого слова мы можем читать И менять значение переменной внутри блока.
- Теперь мы определяем блок, который просто берет цену и количество, и возвращает стоимость объекта.
- Этот кусок кода через блок enumerateKeysAndObjectsUsingBlock: перебирает все объекты в словаре orderItems и использует предыдущий блок для нахождения суммы каждого объекта, затем добавляет это значения в сумму всего заказа (именно поэтому нам нужно ключевое слово __block для переменной заказа — мы меняем ее внутри блока).
- Возвращаем общую сумму заказа.
Возвращаемся в IODOrder.h и добавляем прототип.
- (float)totalOrder;
Последнее, что осталось сделать — добавить подсчет суммы в приложение. Вся грязная работа будет сделана методом totalOrder, нам останется только показать сумму пользователю по нажатию кнопки Total. Заполним метод ibaCalculateTotal:
- (IBAction)ibaCalculateTotal:(UIButton *)sender {
float total = [self.order totalOrder];
UIAlertController *alert = [UIAlertController
alertControllerWithTitle:@"Total"
message:[NSString stringWithFormat:@"$%0.2f",total]
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelButton = [UIAlertAction
actionWithTitle:@"Close"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[alert dismissViewControllerAnimated:YES completion:nil];
}];
[alert addAction:cancelButton];
[self presentViewController:alert animated:YES completion:nil];
}
Тут мы считаем сумму заказа и выводим его на экран.
Вот и все! Запустите проект.
Шпаргалка по блокам
Прежде чем закончить, приведу список блоков, которые могут пригодиться.
NSArray
- enumerateObjectsUsingBlock — возможно, наиболее часто используемый мною блок для перебора объектов.
- enumerateObjectsAtIndexes: usingBlock: — такой же, как и enumerateObjectsUsingBlock, только можно перебирать объекты в определенном промежутке.
- indexesOfObjectsPassingTest: — блок возвращает множество индексов объектов, которые проходят тест, описываемый блоком. Полезно для поиска определенной группы объектов.
NSDictionary
- enumerateKeysAndObjectsUsingBlock: — перебирает элементы словаря.
- keysOfEntriesPassingTest: — возвращает множество ключей, объекты которых прохожят тест, описываемый блоком.
UIView
- animateWithDuration: animations: — блок для UIAnimation, полезно для простых анимаций.
- animateWithDuration: completion: — другой блок UIAnimation. Содержит второй блок для вызова кода по окончании анимации.
Grand Central Dispatch
- dispatch_async — главная функция для асинхронного GCD кода.
Создаем свои собственные блоки
Ниже приведены образцы кода для создания нестандартных блоков.
// Некоторый метод, который принимает блок
- (void)doMathWithBlock:(int (^)(int, int))mathBlock {
self.label.text = [NSString stringWithFormat:@"%d", mathBlock(3, 5)];
}
// вызываем метод с блоком
- (IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
Так как блок — это объект Objective-C, его можно хранить в свойстве для вызова в будущем. Это удобно, когда вы хотите вызвать метод после завершения некоторой асинхронной задачи (связанной с сетью, например).
// объявляем свойство
@property (strong) int (^mathBlock)(int, int);
// храним блок для вызова в дальнейшем
- (void)doMathWithBlock:(int (^)(int, int))mathBlock {
self.mathBlock = mathBlock;
}
// Вызываем метод с блоком
- (IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
// Позже...
- (IBAction)button2Tapped:(id)sender {
self.label.text = [NSString stringWithFormat:@"%d", self.mathBlock(3, 5)];
}
}
Наконец, можно упростить код, используя typedef.
//typedef для блока
typedef int (^MathBlock)(int, int);
// Создаем свойство, используя typedef
@property (strong) MathBlock mathBlock;
// Метод для хранения блока
- (void)doMathWithBlock:(MathBlock) mathBlock {
self.mathBlock = mathBlock;
}
// Вызываем метод с блоком
- (IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
// Позже...
- (IBAction)button2Tapped:(id)sender {
self.label.text = [NSString stringWithFormat:@"%d", self.mathBlock(3, 5)];
}
Блоки и автозавершение
Последний совет. Когда вы используете метод, который принимает блок, Xcode может автозавершить блок за вас, позволяя сэкономить время и предотвратить возможные ошибки.
Например, введите:
NSArray * array;
[array enum
одпрограмма автозаполнения найдет enumerateObjectsUsingBlock — нажмите Enter для автозавершения. Затем нажмите Enter снова для автозавершения блока. Получится вот что:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
code
}
Можно написать свой код вместо code, закрыть вызов метода, и вуаля — намного проще, чем набирать все вручную.
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// Do something
}];
Куда идти дальше?
Код проекта можно скачать здесь. Если вы знакомы с git, можете взять проект отсюда с коммитами на каждом шаге. Надеюсь, в процессе создания этого простого приложения вы смогли почувствовать всю простоту и мощь блоков, и набрались некоторых идей для их использования в своих проектах.
Если это было ваше знакомство с блоками — вы сделали огромный шаг. Блоки нужно из