如何在 iOS 5 中使用 Block (2)

http://www.raywenderlich.com/tutorials

這篇文章來自 iOS 教程團隊成員 Adam Burkepile, 一個全職軟件諮詢顧問和獨立 iOS 開發者。 看看他最新的 app Pocket No Agenda , 或者在 Twitter 上面關注它。

Order up some Storyboards and Blocks in this tutorial!

Order up some Storyboards and Blocks in this tutorial!

歡迎回到 在 iOS 5 中使用 Block 系列教程 – 我們已經有了一些 Storyboard/Interface Builder 方面的實踐!

在這個教程的第一部分,我們用 iOS 5 中的 Storyboard 創建視圖並且建立了一個很好看的界面, 和你在右邊看到的那張截圖差不多。

在這第二部分也是這個系列的最後一部分中,我們要使用 Block了! 我們將會討論 Block 究竟是什麼,它們的語法, 如何使用它們, 並且包含了大量的實例。

我們將會向你展示你怎樣通過 NSArray, UIView 動畫,GCD 來使用 Block, 還有其他更多!

安排好你的時間,並且實踐性和快樂的閱讀吧。

開始:Block 簡介

Block 是 iOS 4.0 和 Mac OSX 10.6 引入的一個新特性。 Block 可以極大的簡化代碼。 他們可以幫助你減少代碼, 減少對代理的依賴, 並且寫出更加簡潔,可讀性強的代碼。

即使有這麼多好處, 還是有很多開發者沒有使用 Block, 因爲他們不知道如何使用。 但是 Block 絕對是你作爲一個 Objective-C 程序員,一定會想要掌握的技能。

讓我們來看看 Block 是誰, 是什麼,在哪裏用它, 爲什麼用它, 還有什麼時候用它。

Block 是什麼東西,它爲什麼那麼重要?

Why do I need these fancy block things?

Why do I need these fancy block things?

Block 的核心就是一段可以在以後的時間裏執行的代碼。

Block 是 first-class functions, 也就是說 Block 是一個標準 Objective-C 對象。 因爲他們是對象, 他們可以作爲參數傳遞, 作爲方法或函數的返回值, 賦值給變量。

在其他語言中,比如 Python,Ruby 和 Lisp, Block 又叫做閉包, 因爲他們包含了定義時的狀態。 Block 會爲所有和它在同一作用範圍內的局部變量創建一個常量拷貝。

在沒有 Block 之前, 如果我們想在之後的某個時間回調一個方法, 你一般會用代理或者 NSNotificationCenter。 這樣也不錯, 除了一點,它會讓你的代碼到處都是 – 你在一個地方開啓了一個任務, 然後在另外一個地方處理它的結果。

Block 是非常不錯的, 因爲它能將和一個任務相關的所有代碼都放在一個地方, 你馬上就會看到。

Block 爲誰準備?

你! Block 是爲每一個人準備的! 嚴格的說, Block 是爲每個人和每個將要用到 Block 的人準備的。 Block 是未來的趨勢, 所以你最好現在也學一下。 很多內建的方法已經用 Block 重寫或者提供了接受 Block 參數的版本。

你怎樣用 Block?
這張 iOS Developer Library 中的圖片很好的解釋了 Block 的語法:

Block 的聲明格式如下:

return_type (^block_name)(param_type, param_type, ...)

如果你之前使用過其他 C 類型的語言,那這段代碼你應該看起來很眼熟, 除了這個 ^ 符號。 ^ 這個符號表示了 “我們定義的這個東西是一個 Block”。

如果你能分析到 ^ 符號的意思 “我是一個 Block” ,那麼祝賀你 – 你已經瞭解了 Block 中最難的部分! :]

注意這裏不需要參數的名稱, 不過,如果你喜歡的話,你也可以加上它們。

下面是定義 Block 的一個例子:

int (^add)(int,int)

下面是 Block 的定義格式:

// Block Definition
^return_type(param_type param_name, param_type param_name, ...) { ... return return_type; }

這就是 Blcok 實際是怎麼創建的。 Block 還有另外一種不同的定義方法。 以 ^ 符號起始,後面跟隨着參數,這裏的參數必須有參數名, 還必須和它要賦值到的 Block 聲明中參數列表裏面的參數類型和順序相匹配。下面是實際的代碼。

當你定義 Block 時, 返回值類型是可選的,並且可以繼承它裏面代碼的返回值類型。  如果它裏面有多條 return 語句,那麼這些語句返回的類型必須都是相同的 (或者強制轉換到相同的類型)。

這裏是 Block 定義的一個例子:

^(int number1, int number2){ return number1+number2 }

如果我們將 Block 的聲明和定義放在一起, 我們會得到這樣一個語句:

int (^add)(int,int) = ^(int number1, int number2){ 
                            return number1+number2;
}

我們可以這樣使用 Block:

int resultFromBlock = add(2,2);

讓我們看一看,使用 Block 和不使用 Block 之間對比的一些例子。

示例: NSArray

讓我們看看 Block 如何改變我們操作數組的方式。

首先,讓我們看一下一般情況下處理循環的方式:

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” 變量,可能會讓你不太明白。 但是如果用 Block 的方式實現它,你就會很清楚的看明白了。 Block 提供了一個 “stop” 變量能讓你在任何時候停止循環,我們簡單的複製了這個功能來支持和 Block 的方式等同的效果。

現在,讓我們看看用快速枚舉的方法實現同樣的功能:

BOOL stop;
int idx = 0;
for (id obj in theArray) {
    NSLog(@"The object at index %d is %@",idx,obj);
    if (stop)
        break;
    idx++;
}

現在,用 Block:

[theArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
    NSLog(@"The object at index %d is %@",idx,obj);
}];

在上面這個基於 Block 的代碼中,你可能會好奇 “stop” 這個變量到底是什麼。 這個變量可以在 block 中賦值爲 YES, 這樣就後續的任何循環都不會繼續了。  這是傳遞到 enumerateObjectsUsingBlock 方法的 Block 中的其中一個參數。

上面這個例子有些微不足道, 而且也很難明顯的體現出 Block 所到來的好處。 但是我想給大家指出 Block 的兩點好處:

  1. 簡單性. 使用 Block 我們可以不寫任何附加的代碼就可以訪問對象,對象在數組中的索引,stop 變量。 這意味着少量的代碼,減少了發生編碼錯誤的機會(當然,並非我們一定會出現編碼錯誤)。
  2. 速度. 使用 Block 在執行速度上要比使用快速枚舉快。 在我們這個例子中,這點微小的速度提升不值得一提,但是在更復雜的情況下,這個優勢就越來越重要。(來源)

示例: UIView Animation

讓我們對一個單獨的 UIView 執行一個簡單的動畫。 它將視圖的透明度調整爲 0,將這個視圖向下和向右移動 50 點。 然後將這個 UIView 從它的 superview 中刪除掉, 很簡單,對嗎?

非 Block 的實現方式:

- (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];
}

Block 的實現方式:

- (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];
                     }];
}

如果我們仔細看一看這兩個方法, 就會發現有 3 個優勢:

  1. 更簡單的代碼  使用 Block, 我們不再需要單獨定義一個回調方法, 或者調用 beginAnimations/commitAnimations 。
  2. 保持代碼在一起  使用 Block, 我們不再需要在一個地方開啓動畫,然後再另外一個地方處理回調。 所有和我們動畫相關的代碼都在一處, 這樣讓他的可讀性和維護性更強。
  3. 蘋果推薦這樣  這裏有一個現實的例子, 蘋果已經用 Block 重寫了之前的一些功能, 現在蘋果官方也推薦,如果可能的話,儘量遷移到基於 Block 的方法上面。 (來源)

什麼時候用 Blocks

我認爲最佳的建議是, 在最合適用 Block 的地方使用它。 這裏你可能會出於向後兼容或者更加熟悉以前的方式的原因,從而還要用老的方法。
但是每次你面臨這種決策的時候, 想一想 Block 是否能讓你更輕鬆以及你是否能用基於 Block 的方式代替現有代碼。 然後選擇一個對你最有價值的方法。

當然,你可能會發現,在以後的時間裏, 你需要越來越多的使用 Block, 因爲大多數框架,無論是第三方的還是蘋果自己的,都正在用 Block 重寫。所以爲了讓你未來更加輕鬆,現在就開始使用 Block 吧。

回到 iOS 晚餐應用: 設置實體類

你將要回顧一下你在第一部分所學到的東西。 如果你沒有看過第一部分或者僅僅需要一個全新的開始, 你可以在這裏下載當前的項目。

在 Xcode 打開項目,切換到 Project Navigator。 右鍵點擊 iOSDiner 然後選擇 New Group。 給他命名爲 “Models”。

右鍵點擊 Models 目錄,然後選擇 New File。 選擇 Objective-C Class。 給這個類命名爲 “IODItem”,讓他繼承自 NSObject。

選擇 iOSDiner 作爲文件的位置, 然後點擊 New Folder 在文件系統中創建一個 Models 目錄。 確保選中這個新創建的 Models 目錄,然後點擊 Create 按鈕。 這會爲 IODItem 創建 .h 和 .m 文件。

用同樣的方式創建 IODOrder 類。 右鍵點擊 Models 目錄,然後點擊 New File。 選擇 Objective-C Class。 類的名稱爲 “IODOrder” ,繼承自 NSObject。

確保 Models 目錄處於選中狀態, 然後點擊 Create 按鈕。

現在所有你需要的類都創建好了, 是時候開始寫代碼了!

設置 IODItem 類的基本屬性

打開 IODItem.h。 首先你要做的是爲這個類添加 NSCopying 協議。

協議是爲一個類指定它要實現什麼方法的一種方式。 一般來說,如果一個類實現了一個協議,那麼這個類就需要實現這個協議中聲明的 required 和 optional 的方法。要實現 NSCopying 協議, 可以這樣修改 IODItem.h:

@interface IODItem : NSObject <NSCopying>

接下來,爲 item 添加一些屬性。 item 有名稱,價格和圖片文件這些屬性。 把下面這些屬性添加到剛纔修改的那行代碼的下面。 現在,完成後的 .h 文件應該是這樣子:

#import <Foundation/Foundation.h>
 
@interface IODItem : NSObject <NSCopying>
 
@property (nonatomic,strong) NSString* name;
@property (nonatomic,assign) float price;
@property (nonatomic,strong) NSString* pictureFile;
 
@end

現在,切換到 IODItem.m 在 @implementation IODItem 的下面添加這些屬性的 @synthesize 聲明。

@synthesize name;
@synthesize price;
@synthesize pictureFile;

如果你現在就編譯構建項目,你將會看到這樣一個警告:

這個警告所指的是你在上面添加的 NSCopying 協議。還記不記得我說的協議可能會定義 required 方法? NSCopying 協議必須實現 -(id)copyWithZone:(NSZone *)zone 方法。 因爲你沒有實現它,這個類是不完整的 – 因此出現了警告!

將下面的代碼添加到 IODItem.m 的結尾(在 @end 之前)。

-(id)copyWithZone:(NSZone *)zone {
    IODItem* newItem = [IODItem new];
    [newItem setName:[self name]];
    [newItem setPrice:[self price]];
    [newItem setPictureFile:[self pictureFile]];
 
    return newItem;
}

哇,沒有任何警告了!

這些代碼所做的就是創建一個新的 IODItem 實例, 將它的屬性設置成和當前的對象中的一樣, 然後返回一個新的實例。

你還需要去設置初始化方法。 這個方法是你在初始化一個新實例時候給對象的屬性設置默認值的一個比較快捷的方式。 在 IODItem.m 的結尾添加如下代碼:

- (id)initWithName:(NSString*)inName andPrice:(float)inPrice andPictureFile:(NSString*)inPictureFile {
    if (self = [self init]) {
        [self setName:inName];
        [self setPrice:inPrice];
        [self setPictureFile:inPictureFile];
    }
 
    return self;
}

切換回到 IODItem.h 在文件的結尾(@end 前面),添加上面那個方法的原型聲明。

- (id)initWithName:(NSString*)inName andPrice:(float)inPrice andPictureFile:(NSString*)inPictureFile;

設置 IODOrder 的基本屬性

下一步,我們將處理另外一個類,IODOrder。 這個類代表了訂單和對訂單的一些操作: 增加訂單項, 刪除訂單項,計算訂單總數和輸出訂單的摘要。

切換到 IODOrder.h, 在 @interface 之前增加下面的代碼,來讓 IODOrder 知道有一個名爲 IODItem 的類。

@class IODItem;

在 @interface 裏面, 增加如下屬性:

@property (nonatomic,strong) NSMutableDictionary* orderItems;

這是一個字典,用於保存用戶提交的訂單。  切換到 IODOrder.m 然後在文件頂部導入 IODItem 類的頭文件。

#import "IODItem.h"

然後在 @implementation IODOrder 的下面聲明這個屬性的 synthesize。

@synthesize orderItems;

設置 IODViewController 的基本屬性

切換到 IODViewController.h 增加一個實例變量和兩個屬性。 將 “@interface IODViewController : UIViewController” 替換成如下:

@class IODOrder;
 
@interface IODViewController : UIViewController {
    int currentItemIndex;
}
 
@property (strong, nonatomic) NSMutableArray* inventory;
@property (strong, nonatomic) IODOrder* order;

currentItemIndex 變量記錄了用戶當前瀏覽的哪個商品。  inventory 這個變量顧名思義, 它是一個包含 IODItem 對象的數組,我們會從 web service 中得到它。 order 是 IODOrder 類的一個實例, 它保存了用戶當前的訂單。

切換到 IODViewController.m 並且做這些事情:

  1. 導入 IODItem 和 IODOrder 類
  2. 爲 inventory 和 order 屬性添加 @synthesize 聲明
  3. 在 viewDidLoad 方法中初始化 currentItemIndex 爲 0
  4. 設置 order 屬性爲一個新的 IODOrder 實例

當你都完成時,它看起來應該是這樣:

#import "IODViewController.h"
#import "IODItem.h"      // <---- #1
#import "IODOrder.h"     // <---- #1
 
@implementation IODViewController
// ... Other synthesize statements ...
 
@synthesize inventory;     // <---- #2
@synthesize order;         // <---- #2
 
// ... didReceiveMemoryWarning - not relevant to discussion ...
 
#pragma mark - View lifecycle
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    currentItemIndex = 0;            // <---- #3
    self.order = [IODOrder new];     // <---- #4
}

構建一下項目。 一切都平穩的運行起來, 沒有任何警告消息。

加載商品清單

你稍後會添加 retrieveInventoryItems 方法, 將會從 web service 中下載和處理商品清單。 這是一個類方法,不是實例方法。

注意: 類方法通過開頭的 + 符號來定義。 實例方法通過 – 符號來定義。

IODItem.m 文件頂部的 #import 下面,增加如下代碼:

#define kInventoryAddress @"http://adamburkepile.com/inventory/"

注意: 如果你使用自己的 web 服務器,修改上面的 URL,指向你自己的服務器。

將下面的方法添加到 IODItem.m 文件的 @end 前面:

+ (NSArray*)retrieveInventoryItems {
    // 1 - Create variables
    NSMutableArray* inventory = [NSMutableArray new];
    NSError* err = nil;
    // 2 - Get inventory data
    NSArray* jsonInventory = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:kInventoryAddress]] 
                                                         options:kNilOptions 
                                                           error:&err];
    // 3 - Enumerate inventory objects
    [jsonInventory enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        NSDictionary* item = obj;
        [inventory addObject:[[IODItem alloc] initWithName:[item objectForKey:@"Name"] 
                                                  andPrice:[[item objectForKey:@"Price"] floatValue]
                                            andPictureFile:[item objectForKey:@"Image"]]];
    }];
    // 4 - Return a copy of the inventory data
    return [inventory copy];
}

你的第一個 Block! 讓我們仔細看看這段代碼,看看它都做了什麼:

  1. 首先,你定義了一個用來存放返回的對象的數組,還有一個 error 指針。
  2. 我們用一個普通的 NSData 對象來從 web service 中下載數據, 然後將這個 NSData 對象傳遞給 iOS 中新的 JSON 數據服務中。 這樣可以將原始數據解析成 Objective-C 中的對象類型((NSArrays, NSDictionaries, NSStrings, NSNumbers, 等等)。
  3. 接下來,我們用之前討論過的 enumerateObjectsUsingBlock: 方法,將這些 NSDictionary 中的普通對象轉換爲 IODItem 類的對象。 我們在 jsonInventory 數組中調用 enumerateObjectsUsingBlock: 方法, 用 Block 遍歷它,然後在裏面將傳遞給 Block 的對象強制轉換爲 NSDictionary 對象。 用這個 NSDictionary 對象來創建新的 IODItem, 最後將這個新的對象添加到要作爲返回值的 inventory 數組中,
  4. 最後,我們返回 inventory 數組。 注意,我們返回了這個數組的一個拷貝,而不是直接返回它, 因爲我們不想返回一個可變數組。 copy 方法創建的是一個不可變數組,你可以安全的返回它。

現在,切換回到 IODItem.h 添加這個方法的原型聲明:

+ (NSArray*)retrieveInventoryItems;

Dispatch Queues 和 Grand Central Dispatch

另外一個對我們很有用的概念就是 dispatch queue。 切換到 IODViewController.m 然後在 @implementation 塊中的 @synthesize 聲明下面,添加如下語句。

dispatch_queue_t queue;

然後, 在 viewDidLoad 方法中的最後一行,添加這行代碼:

queue = dispatch_queue_create("com.adamburkepile.queue",nil);

dispatch_queue_create 方法的第一個參數是隊列的名稱。 你可以用任何方式給它命名, 但它必須在整個系統中是唯一的。 這也是蘋果爲什麼推薦使用反向 DNS 風格的名稱。

你需要在控制器的 dealloc 方法中釋放掉這個隊列。 即便你在項目中使用了 ARC, 但是 ARC 不會管理 dispatch queue, 所以你需要手動的釋放他。但是記住在開啓 ARC 的情況下,你不需要在 dealloc 方法中調用 [super dealloc]。 所以,添加如下代碼:

-(void)dealloc {
        dispatch_release(queue);
}

現在,讓這個隊列運轉起來, 在 viewDidAppear 方法中現有代碼的下面增加如下三行代碼:

	// 1 - Set initial label text
	ibChalkboardLabel.text = @"Loading Inventory...";
	// 2 - Get inventory
	self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
	// 3 - Set inventory loaded text
	ibChalkboardLabel.text = @"Inventory LoadednnHow can I help you?";

運行應用。

有些地方好像不對,是吧? 你通過定義在 IODItem 中的 retrieveInventoryItems 方法來調用 web service, 返回商品清單,並且把他們賦值給 inventory 數組。

要記住, 我們在第一部分中爲這個 PHP web service 設置了5秒的延遲。 但是當我們運行應用時,是不會先顯示 “Loading Inventory…” ,然後等待5秒鐘,再顯示 “Inventory Loaded.” 的。 它實際上是會在應用啓動 5 秒後,直接顯示 “Inventory Loaded”, 不會顯示 “Loading Inventory….” !

這個問題在於: 調用 web service 時, 阻塞和凍結了主線程, 不允許它修改 label 中的文本。 如果有另外一個隊列,你能夠在它上面處理一下需要時間較長的操作, 這樣就不會影響主線程的執行了。

等一下! 我們已經創建了另外一個隊列! 這就是 Grand Central Dispatch 和 Block 幫助我們簡單的解決這個問題的方式。 使用 Grand Central Dispatch, 我們可以將一個任務(以 Block 的形式)指定到我們另外的隊列上, 這樣就不會阻塞主線程了。

viewDidAppear 的第二行和第三行代碼替換成這樣:

	// 2 - Use queue to fetch inventory and then set label text
	dispatch_async(queue, ^{
		self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
		dispatch_async(dispatch_get_main_queue(), ^{            
			ibChalkboardLabel.text = @"Inventory LoadednnHow can I help you?";
		});
	});

注意這裏有兩個不同的 Block, 他們都返回 void 類型,並且不接收參數。

再運行一下應用,一切看起來都很完美。

你是否對我們第二次調用 dispatch_async 來設置 label 的文本感到奇怪? 當你設置 label 的文本時, 你會更新 UI 元素, 任何更新 UI 元素的操作都必須在主線程上面執行。 所以我們再一次調用 dispatch_async, 但這次是在 main queue 上面,並在 main queue 上執行我們的 Block。

在當一個操作需要很長時間,然後還需要後續的更新 UI 的操作時,這種從一個後臺隊列到主隊列的跳轉和嵌套, 是很普遍的。

Grand Central Dispatch 是一個很複雜的系統, 在這個簡短的教程中,你不能完全的領會和理解它。 如果你感興趣的話, 我建議你讀一讀 Multithreading and Grand Central Dispatch on iOS for Beginners 這篇教程。

增加輔助方法

你從 web service 中下載並存儲了商品清單。 現在你將要創建三個輔助方法來幫助你將存儲的商品信息顯示給用戶。

第一個方法是 findKeyForOrderItem:, 將要增加到 IODOrder.m。 這個方法不會有直接的作用, 但是它是訪問 item 字典的必須的方法。

添加如下代碼到 IODOrder.m 類的結尾(在 @end 之前):

- (IODItem*)findKeyForOrderItem:(IODItem*)searchItem {
	// 1 - Find the matching item index
    NSIndexSet* indexes = [[self.orderItems allKeys] indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
        IODItem* key = obj;
        return [searchItem.name isEqualToString:key.name] && 
        searchItem.price == key.price;
    }];
	// 2 - Return first matching item
    if ([indexes count] >= 1) {
        IODItem* key = [[self.orderItems allKeys] objectAtIndex:[indexes firstIndex]];
        return key;
    }
	// 3 - If nothing is found
    return nil;
}

讓我們看看這個函數具體做了什麼。 但是在這之前,我還要解釋一下爲什麼這些是必須的。 IODOrder 包含了一個叫做 orderItems 的屬性, 它是一個鍵-值對字典。 鍵是 IODItem, 而值是一個 NSNumber, 用來表示一個特定的商品被訂購了多少次。

在理論上都沒問題, 但是有一點比較奇怪的是, 當你給 NSDictionary 設置一些鍵的時候, 它不是直接將這個對象賦值過去, 而是創建了這個對象的一個拷貝用作鍵。 這就代表你用作鍵的對象必須遵循 NSCopying 協議。 (這也是爲什麼你之前要給 IODItem 實現 NSCopying 協議的原因)。

事實上 orderItems 字典中的鍵和 inventory 數組中的 IODItem 對象從技術上來說不是同一個對象(即便他們有相同的屬性), 這就意味着你不能通過簡單的比較來搜索鍵。 你必須比較每一個對象的 name 和 price 屬性來確定他們是否是相同的對象。 這也是上面的函數做的事情: 它通過比較鍵的所有屬性來找到我們要搜索的那個。

按照上面說的,這是這些代碼做的事情:

  1. 這裏你遍歷了 orderItems 字典中的所有鍵,並且用 indexesOfObjectsPassingTest: 方法來確定這個鍵的 name 和 price 是否和我們要查找的相匹配。 這也是 Block 的另一個例子。 注意在 ^ 符號後面的 BOOL。 這是返回類型。 這是數組特有的一個方法,並且通過 Block 來比較兩個對象,返回所有符合我們指定的測試條件的對象的索引。
  2. 然後直接得到返回的這些索引, 並且返回這些索引中的第一個。
  3. 如果沒有找到符合條件的鍵,則返回 nil。

不要忘記在 IODOrder.h 中增加方法的原型聲明:

- (IODItem*)findKeyForOrderItem:(IODItem*)searchItem;

現在切換到 IODViewController.m 中,在文件的最後添加如下方法:

- (void)updateCurrentInventoryItem {
    if (currentItemIndex >= 0 && currentItemIndex < [self.inventory count]) {
        IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
        ibCurrentItemLabel.text = currentItem.name;
        ibCurrentItemImageView.image = [UIImage imageNamed:[currentItem pictureFile]];
    }
}

通過 currentItemIndex 和 inventory 數組, 這個方法爲當前選中的商品設置了顯示的名稱和圖片。

還是在 IODViewController.m 中添加:

- (void)updateInventoryButtons {
    if (!self.inventory || [self.inventory count] == 0) {
        ibAddItemButton.enabled = NO;
        ibRemoveItemButton.enabled = NO;
        ibNextItemButton.enabled = NO;
        ibPreviousItemButton.enabled = NO;
        ibTotalOrderButton.enabled = NO;
    } else {
        if (currentItemIndex <= 0) {
            ibPreviousItemButton.enabled = NO;
        } else {
            ibPreviousItemButton.enabled = YES;
        }
        if (currentItemIndex >= [self.inventory count]-1) {
            ibNextItemButton.enabled = NO;
        } else {
            ibNextItemButton.enabled = YES;
        }
        IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
        if (currentItem) {
            ibAddItemButton.enabled = YES;
        } else {
            ibAddItemButton.enabled = NO;
        }
        if (![self.order findKeyForOrderItem:currentItem]) {
            ibRemoveItemButton.enabled = NO;
        } else {
            ibRemoveItemButton.enabled = YES;
        }
        if ([order.orderItems count] == 0) {
            ibTotalOrderButton.enabled = NO;
        } else {
            ibTotalOrderButton.enabled = YES;
        }
    }
}

這是這三個輔助方法中最長的一個, 但也是非常簡單的一個。 這個方法通過查找應用多個可能的狀態,來決定這些按鈕是否可用或禁用。

例如,如果 currentItemIndex 是 0, 前一項按鈕就是禁用的, 因爲你不能再往前了。 如果 orderItems 爲 0, 那麼總訂單數這個按鈕就是禁用的, 因爲沒有用來計算總數的東西。

IODViewController.h 類中添加這兩個方法的原型聲明:

- (void)updateCurrentInventoryItem;
- (void)updateInventoryButtons;

好了! 有了這些輔助方法, 就可以看看效果了。  回到 IODViewController.m 中的 viewDidAppear 方法, 在第一行代碼前面添加如下語句:

	// 0 - Update buttons
    [self updateInventoryButtons];

然後,將第二部分替換成下面這樣:

	// 2 - Use queue to fetch inventory and then update UI
	dispatch_async(queue, ^{
		self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
		dispatch_async(dispatch_get_main_queue(), ^{
			[self updateInventoryButtons];
			[self updateCurrentInventoryItem];
			ibChalkboardLabel.text = @"Inventory LoadednnHow can I help you?";
		});
	});

構建並且運行項目:

哈哈! 漢堡包。。。 我還希望看到其他食物, 所以讓我們讓那些按鈕也工作起來。

當你在 storyboard 中創建好 action 後, ibaLoadNextItem:ibaLoadPreviousItem: 方法就也跟着建立好了。 接下來,我們將如下代碼添加到這些方法中:

- (IBAction)ibaLoadPreviousItem:(id)sender {
    currentItemIndex--;
    [self updateCurrentInventoryItem];
    [self updateInventoryButtons];
}
 
- (IBAction)ibaLoadNextItem:(id)sender {
    currentItemIndex++;
    [self updateCurrentInventoryItem];
    [self updateInventoryButtons];
}

通過你上面創建的輔助方法的幫助, 切換商品僅僅需要改變一下 currentItemIndex 的值,然後刷新一下屏幕顯示就可以了。 沒有比這更容易的吧? 現在你有了一餐廳的食物供你選擇!

編譯一下,然後看看在菜單中切換食物是多麼的簡單。

增加和刪除當前商品

很不幸,你有了一個菜單,但是服務員不能下訂單。 或者,換一種說法, 添加/刪除 按鈕不管用。 是時候修改它了。

你需要在 IODOrder 類中定義另外一個輔助方法, 切換到 IODOrder.m 並且增加如下方法:

- (NSMutableDictionary *)orderItems{
    if (!orderItems) {
        orderItems = [NSMutableDictionary new];
    }
    return orderItems;
}

這僅僅是一個 orderItems 的 getter 方法。 如果 orderItems 被賦了值, 它將返回那個對象。 如果它還沒被賦值, 它會創建一個新的字典然後將它賦給 orderItems, 並且返回它。

接下來你要修改 orderDescription 方法。 這個方法將會提供你要打印在黑板上面的字符串。 將如下代碼添加到 IODOrder.m 中:

- (NSString*)orderDescription {
	// 1 - Create description string
    NSMutableString* orderDescription = [NSMutableString new];
	// 2 - Sort the order items by name
    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 - Enumerate items and add item name and quantity to description
    [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 order description
    return [orderDescription copy];
}

我將分開來講解這些:

  1. 這個字符串是用來描述訂單的。 訂單中的所有商品都會被添加到這個字符串中。
  2. 這段代碼得到一個由 orderItems 字典中的鍵所組成的數組,並且用一個 Block 方法 sortedArrayUsingComparator: 來根據這些鍵的 name 屬性進行排序。
  3. 然後對這個已經排序好的數組調用 enumerateObjectsUsingBlock: 方法。 將每一個鍵都轉換成 IODItem 對象, 得到它對應的值(訂單的數量), 然後將這個字符串添加到 orderDescription 上面。
  4. 最後,返回 orderDescription 字符串, 但是你返回的是它的一個拷貝,一個不可修改的版本。

切換到 IODOrder.h 添加這兩個方法的原型聲明:

- (NSMutableDictionary *)orderItems;
- (NSString*)orderDescription;

現在你可以從 order 對象中得到當前訂單的字符串了, 切回到 IODViewController.m 添加一個方法來調用它。 你可以將這個方法添加到文件的末尾。

- (void)updateOrderBoard {
    if ([order.orderItems count] == 0) {
        ibChalkboardLabel.text = @"No Items. Please order something!";
    } else {
        ibChalkboardLabel.text = [order orderDescription];
    }
}

這個方法查看訂單中的商品數量, 如果數量爲 0, 它返回一個靜態字符串用來表示訂單中沒有任何商品。 另一種情況, 這個方法使用定義在 IODOrder 中的 orderDescription 方法返回的一個代表訂單中所有商品數量清單的一個字符串。

IODViewController.h 中增加方法的原型聲明:

- (void)updateOrderBoard;

現在你可以根據當前的訂單來更新黑板顯示了, 替換 IODViewController.m 中的 viewDidAppear 方法裏面的第二部分:

	// 2 - Use queue to fetch inventory and then then update UI
	dispatch_async(queue, ^{
		self.inventory = [[IODItem retrieveInventoryItems] mutableCopy];
		dispatch_async(dispatch_get_main_queue(), ^{
			[self updateOrderBoard]; // <---- Add
			[self updateInventoryButtons];
			[self updateCurrentInventoryItem];
			ibChalkboardLabel.text = @"Inventory LoadednnHow can I help you?";
		});
	});
}

我發現這個有點無意義,因爲你在後面那行代碼中又給這個 label 設置了初始值,但是處於一致性的考慮,這也不是一個壞主意。

下一個你要實現的方法將會把商品添加到訂單中去。 切換到 IODOrder.m 並添加這個方法:

- (void)addItemToOrder:(IODItem*)inItem {
	// 1 - Find item in order list
    IODItem* key = [self findKeyForOrderItem:inItem];
	// 2 - If the item doesn't exist, add it
    if (!key) {
        [self.orderItems setObject:[NSNumber numberWithInt:1] forKey:inItem];
    } else {
		// 3 - If item exists, update the quantity
        NSNumber* quantity = [self.orderItems objectForKey:key];
        int intQuantity = [quantity intValue];
        intQuantity++;
		// 4 - Update order items list with new quantity
        [self.orderItems removeObjectForKey:key];
        [self.orderItems setObject:[NSNumber numberWithInt:intQuantity] forKey:key];
    }
}

一步一步的講解:

  1. 你用之前創建過的方法來找到這個商品在訂單中所對應的 key。 記住,如果這個對象沒有找到,它會返回一個 nil。
  2. 如果在訂單中沒有找到這個對象, 那麼將這個商品的鍵添加到訂單中,並將它所對應的值設置爲1。
  3. 如果找到了這個對象, 我們得到數量值,存放到一個變量中,並且加 1。
  4. 最後,我們刪除之前的鍵值,並且用剛剛更新過的數量值,增加一個新的鍵值對。

removeItemFromOrder: 方法和 addItemToOrder: 方法非常相似。 在 IODOrder.m 中增加如下代碼:

- (void)removeItemFromOrder:(IODItem*)inItem {
	// 1 - Find the item in order list
    IODItem* key = [self findKeyForOrderItem:inItem];
	// 2 - We remove the item only if it exists
    if (key) {
		// 3 - Get the quanity and decrement by one
        NSNumber* quantity = [[self orderItems] objectForKey:key];
        int intQuantity = [quantity intValue];
        intQuantity--;
		// 4 - Remove object from array
        [[self orderItems] removeObjectForKey:key];
		// 5 - Add a new object with updated quantity only if quantity > 0
        if (intQuantity > 0)
            [[self orderItems] setObject:[NSNumber numberWithInt:intQuantity] forKey:key];
    }
}

注意一下,我們從訂單中刪除商品時,只需要在找到這個對象的時候才進行操作。 如果找到這個商品, 我們得到它的數量, 並且減1,如果減1之後數量大於0, 那麼就刪除鍵值對, 然後重新用新的數量值插入一個新的鍵值對。

切換到 IODOrder.h 增加原型聲明:

- (void)addItemToOrder:(IODItem*)inItem;
- (void)removeItemFromOrder:(IODItem*)inItem;

現在我們切換到 IODViewController.m 並且在 add 和 remove 兩個事件中調用我們剛剛創建的輔助方法:

- (IBAction)ibaRemoveItem:(id)sender {
    IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
    [order removeItemFromOrder:currentItem];
    [self updateOrderBoard];
    [self updateCurrentInventoryItem];
    [self updateInventoryButtons];
}
 
- (IBAction)ibaAddItem:(id)sender {
    IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
    [order addItemToOrder:currentItem];
    [self updateOrderBoard];
    [self updateCurrentInventoryItem];
    [self updateInventoryButtons];
}

對於所有這兩個方法, 我們所做的都是得到 inventory 數組中當前的商品, 將這個對象傳遞給定義在 IODOrder 中的 addItemToOrder: 或 removeItemFromOrder: 方法,並且通過輔助方法來更新 UI 顯示。

再次構建和運行項目, 你應該看到,你現在可以向訂單中增加商品,並且黑板上面會更新你的訂單內容。

UIAnimation

讓我們回顧一下,並且用另外一個 Block 方法來增加一些可視效果。 替換 ibaRemoveItem: 和 ibaAddItemMethod: 方法的代碼:

- (IBAction)ibaRemoveItem:(id)sender {
    IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
    [order removeItemFromOrder:currentItem];
    [self updateOrderBoard];
    [self updateCurrentInventoryItem];
    [self updateInventoryButtons];
 
    UILabel* removeItemDisplay = [[UILabel alloc] initWithFrame:ibCurrentItemImageView.frame];
    [removeItemDisplay setCenter:ibChalkboardLabel.center];
    [removeItemDisplay setText:@"-1"];
    [removeItemDisplay setTextAlignment:UITextAlignmentCenter];
    [removeItemDisplay setTextColor:[UIColor redColor]];
    [removeItemDisplay setBackgroundColor:[UIColor clearColor]];
    [removeItemDisplay setFont:[UIFont boldSystemFontOfSize:32.0]];
    [[self view] addSubview:removeItemDisplay];
 
    [UIView animateWithDuration:1.0
                     animations:^{
                         [removeItemDisplay setCenter:[ibCurrentItemImageView center]];
                         [removeItemDisplay setAlpha:0.0];
                     } completion:^(BOOL finished) {
                         [removeItemDisplay removeFromSuperview];
                     }];
 
}
 
- (IBAction)ibaAddItem:(id)sender {
    IODItem* currentItem = [self.inventory objectAtIndex:currentItemIndex];
    [order addItemToOrder:currentItem];
    [self updateOrderBoard];
    [self updateCurrentInventoryItem];
    [self updateInventoryButtons];
 
    UILabel* addItemDisplay = [[UILabel alloc] initWithFrame:ibCurrentItemImageView.frame];
    [addItemDisplay setText:@"+1"];
    [addItemDisplay setTextColor:[UIColor whiteColor]];
    [addItemDisplay setBackgroundColor:[UIColor clearColor]];
    [addItemDisplay setTextAlignment:UITextAlignmentCenter];
    [addItemDisplay setFont:[UIFont boldSystemFontOfSize:32.0]];
    [[self view] addSubview:addItemDisplay];
 
    [UIView animateWithDuration:1.0
                     animations:^{
                         [addItemDisplay setCenter:ibChalkboardLabel.center];
                         [addItemDisplay setAlpha:0.0];
                     } completion:^(BOOL finished) {
                         [addItemDisplay removeFromSuperview];
                     }];
}

上面的東西看起來代碼量很大,但是它其實是很簡單的。 我們添加的新代碼的第一部分僅僅是創建了一個 UILabel 並且設置了它的一些屬性。 第二部分是一個動畫,移動我們剛剛創建的 UILabel。 這是我們在教程的開始描述的 Block 視圖動畫的一個例子。

編譯並且運行,當你在每次點擊 “+1″ 或 “-1″ 按鈕,增加和刪除商品的時候,你將會看到一個漂亮的動畫。

得到總數

我們將要給 IODOrder.m 添加的最後一個輔助方法是用來得到訂單中商品的總額的:

- (float)totalOrder {

	// 1 - Define and initialize the total variable

    __block float total = 0.0;

	// 2 - Block for calculating total

    float (^itemTotal)(float,int) = ^float(float price, int quantity) {

        return price * quantit
發佈了4 篇原創文章 · 獲贊 4 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章