回到 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 前面),添加上面那个方法的原型声明。
设置 IODOrder 的基本属性
下一步,我们将处理另外一个类,IODOrder。 这个类代表了订单和对订单的一些操作:
增加订单项,删除订单项,计算订单总数和输出订单的摘要。
切换到 IODOrder.h, 在 @interface 之前增加下面的代码,来让 IODOrder 知道有一个名为 IODItem 的类。
在 @interface 里面, 增加如下属性:
这是一个字典,用于保存用户提交的订单。
切换到 IODOrder.m 然后在文件顶部导入 IODItem 类的头文件。
然后在 @implementation IODOrder 的下面声明这个属性的 synthesize。
设置 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 并且做这些事情:
- 导入 IODItem 和 IODOrder 类
- 为 inventory 和 order 属性添加 @synthesize 声明
- 在 viewDidLoad 方法中初始化 currentItemIndex 为 0
- 设置 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! 让我们仔细看看这段代码,看看它都做了什么:
- 首先,你定义了一个用来存放返回的对象的数组,还有一个 error 指针。
- 我们用一个普通的 NSData 对象来从 web service 中下载数据, 然后将这个 NSData 对象传递给 iOS 中新的 JSON 数据服务中。 这样可以将原始数据解析成 Objective-C 中的对象类型((NSArrays, NSDictionaries, NSStrings, NSNumbers, 等等)。
- 接下来,我们用之前讨论过的 enumerateObjectsUsingBlock: 方法,将这些 NSDictionary 中的普通对象转换为 IODItem 类的对象。 我们在 jsonInventory 数组中调用 enumerateObjectsUsingBlock: 方法, 用 Block 遍历它,然后在里面将传递给 Block 的对象强制转换为 NSDictionary 对象。 用这个 NSDictionary 对象来创建新的 IODItem, 最后将这个新的对象添加到要作为返回值的 inventory 数组中,
- 最后,我们返回 inventory 数组。 注意,我们返回了这个数组的一个拷贝,而不是直接返回它, 因为我们不想返回一个可变数组。 copy 方法创建的是一个不可变数组,你可以安全的返回它。
现在,切换回到 IODItem.h 添加这个方法的原型声明:
+ (NSArray*)retrieveInventoryItems;
|
Dispatch Queues 和 Grand Central Dispatch
另外一个对我们很有用的概念就是 dispatch queue。 切换到 IODViewController.m 然后在 @implementation 块中的 @synthesize 声明下面,添加如下语句。
然后, 在 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 属性来确定他们是否是相同的对象。 这也是上面的函数做的事情: 它通过比较键的所有属性来找到我们要搜索的那个。
按照上面说的,这是这些代码做的事情:
- 这里你遍历了 orderItems 字典中的所有键,并且用 indexesOfObjectsPassingTest: 方法来确定这个键的 name 和 price 是否和我们要查找的相匹配。 这也是 Block 的另一个例子。 注意在 ^ 符号后面的 BOOL。 这是返回类型。 这是数组特有的一个方法,并且通过 Block 来比较两个对象,返回所有符合我们指定的测试条件的对象的索引。
- 然后直接得到返回的这些索引, 并且返回这些索引中的第一个。
- 如果没有找到符合条件的键,则返回 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 并且增加如下方法:
这仅仅是一个 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]; }
|
我将分开来讲解这些:
- 这个字符串是用来描述订单的。 订单中的所有商品都会被添加到这个字符串中。
- 这段代码得到一个由 orderItems 字典中的键所组成的数组,并且用一个 Block 方法 sortedArrayUsingComparator: 来根据这些键的 name 属性进行排序。
- 然后对这个已经排序好的数组调用 enumerateObjectsUsingBlock: 方法。 将每一个键都转换成 IODItem 对象, 得到它对应的值(订单的数量), 然后将这个字符串添加到 orderDescription 上面。
- 最后,返回 orderDescription 字符串, 但是你返回的是它的一个拷贝,一个不可修改的版本。
切换到 IODOrder.h 添加这两个方法的原型声明:
现在你可以从 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]; } }
|
一步一步的讲解:
- 你用之前创建过的方法来找到这个商品在订单中所对应的 key。 记住,如果这个对象没有找到,它会返回一个 nil。
- 如果在订单中没有找到这个对象, 那么将这个商品的键添加到订单中,并将它所对应的值设置为1。
- 如果找到了这个对象, 我们得到数量值,存放到一个变量中,并且加 1。
- 最后,我们删除之前的键值,并且用刚刚更新过的数量值,增加一个新的键值对。
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