iOS 5 Storyboard 入门-1

  1. 云栖社区>
  2. 博客>
  3. 正文

iOS 5 Storyboard 入门-1

悟静 2014-04-24 07:11:00 浏览573
展开阅读全文


 Ray Wenderlich 

这篇文章还可以在这里找到 英语法语日语俄语西班牙语阿拉伯语

If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

Serve yourself with some storyboards!

来自Ray: 这是 iOS 5盛宴 中的第二篇教程, 这篇教程是我们的新书 iOS 5 By Tutorials 的一个免费预览章节, Matthijs Hollemans 写了这个章节,他也是 iOS Apprentice Series 的作者

这篇教程来自iOS 教程团队成员 Matthijs Hollemans, 一个资深的iOS开发者和设计师

Storyboard 是iOS 5 中令人兴奋的一个新特性,他将为你在创建用户界面上节省很多时间。 那么究竟什么是Storyboard呢?我将用一幅图片来向你展示: 下面这个就是本教程中即将用到的Storyboard。

The full storyboard we'll be making in this tutorial.

你或许不能精确的知道这个应用是做什么的,但是你可以清楚的看到它有哪些屏幕界面,这些屏幕界面之间是怎样互相关联的。这就是Storyboard的强大之处。

如果你的应用有很多个不同的屏幕界面,Storyboard则可以减少那些用于在这些界面之前来回切换的中间代码(glue code)。 现在你的应用,用一个Storyboard就可以包含所有控制器的界面设计和他们之间的关系, 而不再需要为每一个控制器分别再创建一个nib文件。

Storyboard和普通的nib相比有很多优点。

  • 借助Storyboard,你可以对你应用中所有的界面和它们之间的联系有一个更好的概念上的总览。 因为所有的设计都在单个文件中,而不是分布成许多nib文件, 可以更加容易的找到任何东西。
  • Storyboard表明了各个界面之间的切换规则。 这些切换规则叫做“segues”, 按住ctrl键,从一个控制器拖动到另一个就可以创建它们。多亏了segues,能让你用更少的代码来处理UI。
  • Storyboard让UITableView使用起来更加简单, 它提供了原型单元格(prototype cells)和静态单元格(static cells). 你几乎可以完全在Storyboard编辑器中来设计你的UITableView,大大减少了你的代码量。

并不是所有的事情都那么完美,当然,Storyboard也有一些局限性。 Storyboard编辑器还没有像Interface Builder那么强大,还有少数的一些功能,IB可以实现,但是Storyboard编辑器不能完成。你也需要一个大显示器,特别是在设计iPad应用时。

如果你是那种讨厌使用Interface Builder,只愿意编码实现整个UI的人, 那么Storyboard大概不是为你准备的。 从我个人来说,我更希望代码量越少越好,特别是UI代码,所以这个工具对我来说可是个好东西。

你还可以在iOS 5 和Xcode 4.2中使用nib文件。 虽然我们现在有了Storyboard,使用Interface Builder也不是不可以。 如果你要继续用nib并且一直用下去, 但是你也可以将Storyboard和nib一起使用。 这不是一个必须二选一个问题。

在本教程中,我们将会看到你可以用Storyboard来做什么。 我们将要创建的应用可能会看起来没什么意义,但是它向你展示Storyboard最常用的那些操作。

开始吧

打开Xcode并且创建一个新项目。 使用Single View Application模板作为我们的起点。然后从这里开始构建我们的应用。

Xcode template options

如下填写模板选项:

  • Product Name: Ratings
  • Company Identifier: the identifier that you use for your apps, in reverse domain notation
  • Class Prefix: leave this empty
  • Device Family: iPhone
  • Use Storyboard: check this
  • Use Automatic Reference Counting: check this
  • Include Unit Tests: this should be unchecked

Xcode创建好项目后,Xcode主窗口显示如下:

Xcode window after creating project

我们的新项目里有两个类,AppDelegate和ViewController, 还有这个教程的主角 MainStoryboard.storyboard。 注意了,这次项目中没有 .xib 文件,也没有MainWindow.xib。

让我们看一看Storyboard。 在左边的项目导航中点击MainStoryboard.storyboard 来打开Storyboard编辑器:

Storyboard editor

Storyboard编辑器看起来很像Interface Builder。 你可以从Object Library(屏幕右下角)中拖动控件到View Controller中来设计布局。 不同的是,Storyboard不是仅仅包含一个控制器,而是你应用中所有的控制器。

控制器,在Storyboard的官方术语中叫做”scene”, 不过 “scene“ 其实就是一个视图控制器。 在这之前,你会为每一个Scene/控制器来创建一个nib文件, 但是现在所有的这些都包含在一个Storyboard中。

在iPhone中一次只能显示这些scene中的一个, 但是在iPad中,你可以同时显示多个, 例如UISplitView或者是Popover。

为了进一步的感受编辑器是如何工作的, 拖动一下控件到控制器的空白区域吧。

Dragging controls from Object Library

左边栏是文档的大纲:

Document outline

在Interface Builder中,这个区域列出了nib中的组件,但是在Storyboard编辑器中,他显示了所有的视图控制器中的内容。现在我们的Storyboard中仅仅有一个视图控制器,但是随着教程的演进,我们将会添加更多的控制器。

在 Scene 下方,有一个迷你版的文档大纲,叫做Dock:

The dock in the Storyboard Editor

Dock显示了scene中最顶级的组件。 每个scene至少有一个First Responder和一个View Controller对象, 但是他也可以拥有其他的顶级组件。 后面会更多的讲解。 Dock可以方便的建立连接。 如果你想将一些其他的东西连接到控制器, 你可以把他的图标拖动到Dock上面。

注意: 你或许没怎么用过 First Responder 。 这是一个代理对象,用于指向当前作为第一事件响应者(first responder)的对象。 它也出现在 Interface Builder 中, 你看可能从来没有用过它。 举个例子,你可以将一个按钮的 Touch Up Inside 事件绑定到 First Responder 的 cut: selector 上。 如果在某一时刻,一个文本框得到了焦点, 那么你就可以按下那个按钮,让这个作为First Responder 的文本框,来剪切它里面的文本到剪贴板上。

运行这个应用, 你应该可以看到它显示的内容和我们在编辑器中设计的完全一样:

App with objects

如果你之前创建过基于nib的应用,那么你会一直有一个MainWindow.xib文件。 这个nib文件包含了一个顶级元素UIWindow, 一个App Delegate的引用,和一个或多个视图控制器。 当你将应用的UI搬到Storyboard后, 就不再需要MainWindow.xib文件了。

No more MainWindow.xib

那么,如果没有MainWindow.xib文件, Storyboard是如何被加载的呢?

让我们来看一下应用的delegate文件, 打开AppDelegate.h, 你将会看到如下内容:

#import <UIKit/UIKit.h>
 
@interface AppDelegate : UIResponder <UIApplicationDelegate>
 
@property (strong, nonatomic) UIWindow *window;
 
@end

要使用Storyboard,你的应用代理对象就必须继承UIResponder(以前都是直接继承自NSObject),并且还有一个UIWindow属性(和以前相比,这个属性不再是一个IBOutlet)。

如果你再看一下 AppDelegate.m , 你会发现它没有做任何事情, 所有的方法都是空的。 即使是 application:didFinishLaunchingWithOptions: 方法, 也只不过简单的返回了一个YES。 在以前,这里会把主视图控制器的视图添加到window上面,或者将window设置到rootViewController属性上面,但是现在,这些都不需要了。

这个秘密就在于Info.plist文件中, 点击Ratings-Info.plist文件(在Supporting Files分组中), 你将会看到下面的内容:

Setting the main storyboard file base name in Info.plist

在基于nib的项目中,这里会有一个名为NSMainNibFile键,或者叫”Main nib file base name”, 它会告诉UIApplication去加载MainWindow.xib, 然后把它关联到应用中。 而我们现在的Info.plist已经没有这些设置了。

Storyboard应用会使用一个叫做UIMainStoryboardFile的键,或者叫做“Main storyboard file base name”, 来指定应用启动时要加载的Storyboard名称。 当检测到这个设置后,UIApplication将会加载 MainStoryboard.storyboard 文件,并且自动实例化其中的第一个视图控制器, 同时把它的所有视图放到一个新的UIWindow对象中。 不需要写任何代码。

你也可以在Target Summary中看到这些:

Setting Main Storyboard in Target summary

这里面有一个新的iPhone/iPod Deployment Info选项让你来选择是使用Storyboard还是nib文件来启动应用。

为了保持教程的完整性,让我们再来看看main.m里面有什么:

#import <UIKit/UIKit.h>
 
#import "AppDelegate.h"
 
int main(int argc, char *argv[])
{
	@autoreleasepool {
		return UIApplicationMain(argc, argv, nil, 
			NSStringFromClass([AppDelegate class]));
    }
}

在以前 UIApplicationMain() 函数的最后一个参数是一个nil值,现在他是NSStringFromClass([AppDelegate class])。

和使用MainWindow.xib最大的不同就是,应用代理不是Storyboard的一部分。因为应用代理不再从nib文件中加载,我们就必须告诉UIApplicationMain我们的应用代理的名字,否则就找不到它了。

把它添加到我的标签上

我们的评分应用使用一个有两个标签的标签化界面。通过Storyboard可以非常容易的创建标签。

切换回到MainStoryboard.storyboard,从Object Library中拖动一个Tab Bar Controller到设计器中。 你或许想把Xcode的窗口最大化,因为Tab Bar Controller还包含了两个视图控制器,所以你需要一些空间来摆放他们。

Adding a new tab bar controller into the Storyboard

这个新的Tab Bar Controller包含了两个预先配置好的子控制器,每个都对应了它的一个Tab。UITabBarController是一个容器控制器,因为他包含了一个或多个的子控制器。 还有其他两个容器分别是Navigation Controller和Split View Controller(我们将在后面看到它们)。 iOS 5 另外一个很酷的新增特性就是提供了一个新的API让你可以写自己的容器控制器。 在本书的后续章节,我们会有一个教程来说明它。

在Storyboard编辑器中,通过箭头连接Tab Bar Controller和它包含的视图控制器来表示容器中的关系。

Relationship arrow in the Storyboard editor

注意:如果你想同时移动Tab Bar controller和它的子控制器,你可以按住Command键并点击选择多个scene,然后再移动它们(选中的scene会有一个浅蓝色的边框)。

拖动一个Label到第一个视图控制器中,设置他的文本为”First Tab”。 同样也拖动一个Label到第二个控制器中,并且设置他的文本为”Second Tab”。 这样我们就可以清楚的看到在切换Tab时发生了什么。

注意: 当编辑器被缩放时,你不能拖动任何东西到scene中, 所以,你首先需要返回到默认缩放级别中。

选中Tab Bar Controller并且找到Attributes Inspector。 选中内容为Initial View Controller复选框。

Is Initial View Controller attribute

在编辑面板中,原来指向普通控制器的那个箭头,现在指向了Tab Bar Controller:

Arrow indicating initial view controller in Storyboard editor

这样,当我们启动应用的时候,UIApplication就会将Tab Bar Controller作为我们应用的第一个界面。

Storyboard总是会有一个试图控制器用作初始控制器, 作为Stroyboard的入口。

启动这个应用试一试, 这个应用现在有一个Tab Bar并且你可以在这两个Tab之间来回切换:

App with tab bar

Xcode实际上有一个用来创建标签化应用的模板来供我们使用(不出意料,叫做Tabbed Application template),但是最好还是要知道它的原理, 这样在必要的时候,你也可以手动创建它。

你可以删除那个被模板默认添加进来的控制器,我们已经不再需要它了。 现在Storyboard仅包含了Tab Bar和它的两个子控制器。

顺便说一下,如果你连接多于5个scene到Tab Bar Controller中,它会自动放置一个 “更多” 标签,非常漂亮。

增加一个Table View Controller

现在附加到Tab Bar Controller的两个Scene都是普通的UIViewController。 我想把第一个标签的scene替换成一个UITableViewController。

点击第一个控制器, 选中它并且删除它。 从Object Library中拖出一个新的Table View Controller到面板中。

Adding a new table view controller to the Storyboard

当Table View Controller被选中后,在Xcode的菜单栏中选择 EditorEmbed InNavigation Controller。 这样会增加另外一个控制器到面板中:

Embedding in a navigation controller

你也可以从Object Library中拖出一个Navigation Controller, 但是Embed In这个方式更加简单。

因为Navigation Controller也是一个容器控制器(和Tab Bar Controller一样), 它有一个箭头指向Table View Controller。你也可以在文档大纲中看到他们之间的关系。

View controller relationships in outline of Storyboard editor

注意到,嵌入的Table View Controller包含有一个navigation bar。 Storyboard自动添加了这个,因为这个Scene将会被显示到Navigation Controller的框架中。 它不是一个真正的UINavigationBar, 而是用来模拟的。

如果你看一下Table View Controller的Attributes Inspector,可以在最上面看到模拟选项:

Simulated metrics in Storyboard editor

“Inferred” 是Storybard的默认设置, 它的意思是当一个Scene包含在navigation controller中的时候,将会显示一个navigation bar, 同样的,在tab bar controller中,会显示一个tab bar。 你也可以覆盖这个设置, 但要记住,他们只是用来帮你设计界面的。 这些模拟选项不会在运行时实际的出现,他们只是在设计界面时用来模拟运行时的最终效果。

让我们把这些新的Scene连接到Tab Bar Controller中。 按住Ctrl然后从Tab Bar Controller拖动到Navigation Controller:

Connecting scenes in the storyboard

当你这样做之后,一个小的弹出框显示出来:

Create relationship segue

选择 “Relationship – viewControllers” 这个选项。这样会在这两个Scene中创建一个新的关系:

Relationship arrow in the Storyboard editor

Tab Bar Controller有两个这样的关系,每个Tab对应一个。 Navigation Controller自己也有一个关系连接到Table View Controller。 还有另外一种箭头, 叫做Segue,我们在后面就会讨论它。

当我们建立了这个新的连接, 一个新的Tab就会被添加到Tab Bar Controller, 叫做 “Item”. 我想让这个新的Scene作为第一个Tab,所以,拖动这些Tab来改变它们的顺序:

Rearranging tabs in the Storyboard editor

运行应用看一下, 第一个Tab现在包含了一个里面有table view的navigation controller。

App with table view

在我们给应用增加一些实际功能之前,让我们稍微整理一下Storyboard。 我想要把第一个Tab的名字改成”Players”,把第二个改成”Gestures”。 你不能通过Tab Bar Controller来改变他们, 但是你可以通过连接到每个Tab的视图控制器来改变他们。

当你将一个视图控制器连接到Tab Bar Controller后, 它会得到一个Tab Bar Item对象,你可以用Tab Bar Item来设置tab的标题和图片。

在Navigation Controller中,选择Tab Bar Item, 在Attributes Inspector中设置它的Title为 “Players”:

Setting the title of a Tab Bar Item

把第二个Tab Bar Item的名称改为 “Gestures”。

我们应该也需要在这些tab上面放置一些图片。在教程资源中包含了一个Images子目录。 把这个目录添加到项目中。 在名为”Players”的 Tab Bar Item 的 Attributes Inspector 中,选择Players.png 这张图片。 你可能已经猜到了, 也要为”Gestures” 这个Tab Item设置Gestures.png这张图片。

同样的,Navigation Controller中的子控制器,也有一个Navigation Item用来配置导航条。 选中Table View Controller中的Navigation Item, 将它的Title改为”Players”。

另外,你还可以双击导航条来改变它的Title。(注意:你应该双击Table View Controller中模拟的导航条,而不是Navigation Controller中的那个。)

Changing the title in a Navigation Item

运行应用,令人惊叹的事情发生了, 所有这些都没有写一行代码!

App with the final tabs

原型单元格

你可能注意到了,当我们添加Table View Controller之后,Xcode出现了这样的警告:

Xcode warning: Prototype cells must have reuse identifiers

警告的消息如是这样,“Unsupported Configuration: Prototype table cells must have reuse identifiers”。当你添加Table View Controller到storyboard后, 它默认会使用原型单元格,但是我们并没有正确的配置它,所以才会出现警告。

原型单元格是Storyboard和相比nib之下,提供的一项非常酷的优势特性。 以前, 如果你需要一个自定义的单元格,你只能用代码为单元格添加子视图,或者为单元格单独创建一个nib文件,然后再通过一些特殊的技巧加载它。 这样还是可行的,但是原型单元格让这一切变得更加简单。 现在,你可以直接在Storyboard编辑器中,设计你自定义的单元格。

Table View Controller默认有一个空的原型单元格。 选中它,然后在Attributes Inspector中设置Style为Subtitle。这样会立即将单元格的外观改变成包含两个Label单元格。 如果你之前使用过TableView并且创建过自己的单元格,你会发现这个和UITableViewCellStyleSubtitle是一样的。 借助原型单元格,你可以像我们刚才那样,使用任何一个内建的单元格样式,或者创建一个你自己的样式(这个是我们一会儿要做的)。

Creating a Prototype cell

设置Accessory属性为Disclosure Indicator, 并且把单元格的Reuse Identifier设置成“PlayerCell”。 这样就可以消除Xcode的警告了。 所有的原型单元格仍然还是一个UITableViewCell对象,所以他们还是应该有一个Reuse Identifier。 Xcode只是确保我们不会忘了它(至少我们中会注意警告的人)。

运行应用, …没有任何改变。 这并不奇怪,我没有为Table指定数据源,所以它还不知道该怎么显示每一行。

在项目中创建一个新文件。选择 UIViewController 模板。将这个类命名为PlayersViewController并且让它继承UITableViewController。 “With XIB for user interface option” 这个选项应该是未选中状态, 因为我们已经在storyboard中设计好了这个控制器。 今天没有nibs!

Creating a view controller with the table view controller template

返回Storyboard编辑器, 选中Table View Controller, 在Identity Inspector 中,设置Class为PlayersViewController。 这是将Storyboard中的Scene和你自己的控制器子类相关联的关键一步。 千万不要忘了这个,否则你自己的类不会被使用。

Setting the class name in the identity inspector

从现在开始,当你运行应用,Storyboard中的table view controller将会是PlayersViewController类的一个实例。

为PlayersViewController.h增加一个可变数组的属性:

#import <UIKit/UIKit.h>
 
@interface PlayersViewController : UITableViewController
 
@property (nonatomic, strong) NSMutableArray *players;
 
@end

这个数组将会包含我们应用主要的数据模型。 它包含Player对象。 让我们现在就创建Player类。 用Objective-C模板增加一个新的文件到项目中。 文件名称叫做Player,继承自NSObject。

按照下面改写Player.h文件。

@interface Player : NSObject
 
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *game;
@property (nonatomic, assign) int rating;
 
@end

修改Player.m文件:

#import "Player.h"
 
@implementation Player
 
@synthesize name;
@synthesize game;
@synthesize rating;
 
@end

这里没什么特别的。 Player是一个简单的容器对象,有三个属性: 玩家的名称,他正在玩的游戏名称,还有评分(1星到5星)。

我们将在App Delegate中创建一个数组和一些用来测试的Player对象,然后把他们赋值给 PlayersViewController的playes属性。

在 AppDelegate.m 中,导入Player和PlayersViewController类,并且增加一个新的实例变量 players。

#import "AppDelegate.h"
#import "Player.h"
#import "PlayersViewController.h"
 
@implementation AppDelegate {
	NSMutableArray *players;
}
 
// Rest of file...

然后修改 didFinishLaunchingWithOptions 方法:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
	players = [NSMutableArray arrayWithCapacity:20];
	Player *player = [[Player alloc] init];
	player.name = @"Bill Evans";
	player.game = @"Tic-Tac-Toe";
	player.rating = 4;
	[players addObject:player];
	player = [[Player alloc] init];
	player.name = @"Oscar Peterson";
	player.game = @"Spin the Bottle";
	player.rating = 5;
	[players addObject:player];
	player = [[Player alloc] init];
	player.name = @"Dave Brubeck";
	player.game = @"Texas Hold’em Poker";
	player.rating = 2;
	[players addObject:player];
	UITabBarController *tabBarController = 
     (UITabBarController *)self.window.rootViewController;
	UINavigationController *navigationController = 
     [[tabBarController viewControllers] objectAtIndex:0];
	PlayersViewController *playersViewController = 
     [[navigationController viewControllers] objectAtIndex:0];
	playersViewController.players = players;
    return YES;
}

这里创建了一些Player对象并把它们添加到players数组中。 而我们是这样做的:

UITabBarController *tabBarController = (UITabBarController *)
  self.window.rootViewController;
UINavigationController *navigationController = 
  [[tabBarController viewControllers] objectAtIndex:0];
PlayersViewController *playersViewController = 
  [[navigationController viewControllers] objectAtIndex:0];
playersViewController.players = players;

那是什么呢? 我们想要把 players 数组赋值给 PlayersViewController 的 players 属性, 然后它就可以用这个数组来作为数据源。 但是应用代理还不知道 PlayersViewController 这个控制器, 所以我们需要通挖掘 Storyboard 来找到它。

这就是 Storyboard 的其中一点局限性。 Interface Builder 总能够在 MainWindow.xib 中让应用代理中引用到, 并且你可以将顶级控制器连接到应用代理的 outlets 中。 目前这个在 Storyboard 中还是不行的。 你不能在应用代理中引用到顶级控制器。 这很不幸, 但是我们还是总能够通过代码来得到这些引用。

UITabBarController *tabBarController = (UITabBarController *)
  self.window.rootViewController;

我们知道,Storybord 的根控制器是一个 Tab Bar Controller, 所以我们可以找到 window 的 rootViewController 并强制转换它。

PlayersViewController 在第一个Tab中的导航控制器内部, 所以我们可以先找到 UINavigationController 对象:

UINavigationController *navigationController = [[tabBarController 
  viewControllers] objectAtIndex:0];

然后将它当做我们要找的 PlayersViewController 的根控制器。

PlayersViewController *playersViewController = 
  [[navigationController viewControllers] objectAtIndex:0];

很不幸, UINavigationController 没有 rootViewController 这个属性, 所以我们需要使用 viewControllers 数组。(它倒有一个 topViewController 属性,但是它指向的是栈中的最顶端控制器, 但我们找的是最下端的。 当应用刚启动的时候,我们可以用 topViewController 来得到它, 但这种情况并不总是成立)。

现在,我们有一个充满 Player 对象的数组, 我们可以继续为 PlayersViewController 构建数据源。

打开 PlayersViewController.m, 并修改Table View 的数据源方法:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 1;
}
 
- (NSInteger)tableView:(UITableView *)tableView 
  numberOfRowsInSection:(NSInteger)section
{
	return [self.players count];
}

真正的功效发生在 cellForRowAtIndexPath 方法。 Xcode 模板生成的代码是这样的:

- (UITableViewCell *)tableView:(UITableView *)tableView 
  cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView 
      dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] 
          initWithStyle:UITableViewCellStyleDefault 
          reuseIdentifier:CellIdentifier];
    }
 
    // Configure the cell...
    return cell;
}

这毫无疑问,是你一直以来创建你自己的 Table View 的代码。 好吧,以后不会再用到了! 将这个方法替换成:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell = [tableView 
      dequeueReusableCellWithIdentifier:@"PlayerCell"];
	Player *player = [self.players objectAtIndex:indexPath.row];
	cell.textLabel.text = player.name;
	cell.detailTextLabel.text = player.game;
    return cell;
}

这看起来简单多了! 得到一个新的单元格,你只需要这一行代码:

UITableViewCell *cell = [tableView 
  dequeueReusableCellWithIdentifier:@"PlayerCell"];

如果当前没有可以重复使用的单元格, 这里将会自动创建一个新的原型单元格,并且返回给你。 你要做的仅仅是提供一下你在Storyboard编辑器中为这个原型单元格指定的重用标识(Reuse Ifentifier), 在我们这个项目中就是 “PlayerCell”。 一定不要忘记设置这个标识,否则这个就不能正常工作了。

因为这个类还不知道 Player 对象, 所以需要在文件顶部,增加一行 #import 语句。

#import "Player.h"

还有,我们别忘了给这个属性加上 synthesize 语句:

@synthesize players;

现在,你可以运行应用了, 看!, Table View 中有了这些玩家信息了:

Table view with data

在这个应用中,我们只用了一个原型单元格, 但是如果你的 Table 需要显示不同种类的单元格, 那么你可以直接添加新的原型单元格到 Storyboard 中。 你可以复制现有的单元格,或者增加Table View 的 Prototype Cells 属性的值。 不过要确保你给每一个原型单元格都指定了重用标识。

仅仅需要一行代码,就可以使用原型单元格了。 我想,这太棒了!

设计你自己的原型单元格

对大多数应用来说,使用标准的单元格样式已经足够了, 但是, 我想在单元格的右边添加一张图片用来表示玩家的评分(用星星表示)。 标准的单元格样式没有提供这样的外观, 所以我们需要创建一个自定义个设计。

切换回到 MainStoryboard.storyboard, 选中Table View 的原型单元格, 设置 Style 属性为 Custom。 默认的Label 都消失了。

首先,我们让单元格的高度大一些, 拖动单元格下边框,或者在 Size Inspector 中改变 Row Height 属性都可以。 我用第二种方法设置单元格的高度为 55 点。

从 Objects Library 中拖出两个 Label 到单元格中, 并且让把两个 Label 放在和之前差不多的位置。 调整一些字体和颜色属性。 把他们的 Highlighted color 属性都设置成 white。 这样在用户点击单元格后单元格的背景变成蓝色时,看起来会好看一点。

拖动一个 Image View 到单元格中,并放置到右边, 紧挨着右边的指示箭头。 设置宽度为81 点, 高度不是很重要。
为了让任何放到这里的图片都不会被拉伸, 设置它的 Mode 属性为 Center (在 Attributes Inspector 中的 View 选项卡中)。

我将Label都设置成210点宽,所以他们不会和 Image View 重叠上。 原型单元格最终的设计看起来是这样的:

Prototype cells with a custom design

因为这是一个自定义单元格, 我们不能再用 UITableViewCell 的 textLabel 和 detailTextLabel 属性来放置文本。 这些属性指向的 Label 已经不在我们的单元格上面了。 我们需要使用 tags 来找到我们的 Label。

将 Name Label 的 tag 设为 100, Game Label 的tag 设为 101, Image View 的 tag 设为 102, 你可以在 Attributes Inspector 中设置它们。

然后打开 PlayersViewController.m , 修改 cellForRowAtIndexPath 方法:

- (UITableViewCell *)tableView:(UITableView *)tableView 
  cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell = [tableView 
      dequeueReusableCellWithIdentifier:@"PlayerCell"];
	Player *player = [self.players objectAtIndex:indexPath.row];
	UILabel *nameLabel = (UILabel *)[cell viewWithTag:100];
	nameLabel.text = player.name;
	UILabel *gameLabel = (UILabel *)[cell viewWithTag:101];
	gameLabel.text = player.name;
	UIImageView * ratingImageView = (UIImageView *)
      [cell viewWithTag:102];
	ratingImageView.image = [self imageForRating:player.rating];
    return cell;
}

这里用到了一个新方法 imageForRating, 把这个方法增加到 cellForRowAtIndexPath 上面:

- (UIImage *)imageForRating:(int)rating
{
	switch (rating)
	{
		case 1: return [UIImage imageNamed:@"1StarSmall.png"];
		case 2: return [UIImage imageNamed:@"2StarsSmall.png"];
		case 3: return [UIImage imageNamed:@"3StarsSmall.png"];
		case 4: return [UIImage imageNamed:@"4StarsSmall.png"];
		case 5: return [UIImage imageNamed:@"5StarsSmall.png"];
	}
	return nil;
}

已经差不多了. 现在重新运行应用吧。

Wrong cell height for custom cells made in Storyboard editor

恩,看起来没什么问题。 我们修改了原型单元格的高度, 但是 Table View 没有自动调整这些。 有两个方法可以修正它: 我们可以修改 Table View 的 Row Height 属性, 或者实现 heightForRowAtIndexPath 方法。 第一个方法更加简单, 让我们这样弄吧。

注意: 如果你提前不知道单元格的高度, 或者每个单元格有不同的高度, 那么你要用 heightForRowAtIndexPath 方法。

回到 MainStoryboard.storyboard, 在 Table View 的 Size Inspector 中, 设置行高为 55:

Setting the table view row height

顺便说一下, 如果你是通过拖动边缘修改的单元格高度, 而不是输入高度值。 那么Table View 的 Row Height 也会自动跟着改变。

如果运行应用, 它看起来会好很多了。

使用原型单元格的子类

我们的 Table View 已经运转的很不错了, 但是我不太喜欢使用tag来访问原型单元格的Label 和其他视图。 如果我们能把Lable 连接到 outlet 上, 然后使用相应的属性, 那就非常方便了。 结果证明, 我们可以:

在项目中增加一个新文件, 使用 Objective-C 模板。 文件名为 PlayerCell , 继承自 UITableViewCell。

修改 PlayerCell.h :

@interface PlayerCell : UITableViewCell
 
@property (nonatomic, strong) IBOutlet UILabel *nameLabel;
@property (nonatomic, strong) IBOutlet UILabel *gameLabel;
@property (nonatomic, strong) IBOutlet UIImageView 
  *ratingImageView;
 
@end

修改 PlayerCell.m 的内容:

#import "PlayerCell.h"
 
@implementation PlayerCell
 
@synthesize nameLabel;
@synthesize gameLabel;
@synthesize ratingImageView;
 
@end

这个类本身没做什么事情, 仅仅是增加了一些属性, nameLabel, gameLabel 和 ratingImageView。

回到 MainStoryboard.storyboard, 选中原型单元格, 在 Identity Inspector 中修改 Class 属性为 “PlayerCell” 。现在当你用 dequeueReusableCellWithIdentifier 来获得新的单元格时, 它将返回而一个 PlayerCell 的实例, 而不再返回 UITableViewCell。

注意,我给这个类指定的名称和重用标识是一样的 — 他们都叫做 PlayerCell — 但这只是我为了保持前后一致。 类名和重用标识并没有什么关系, 所以你也可以给他们指定不同的名字。

现在,你可以将 Label 和 Image View 都连接到这些 outlet 中了。 选择label 从 Connections Inspector 中拖动到 Table View Cell 上面, 或者用另一种方式, 按住 Ctrl从 Table View 中拖回 Label。

Connecting the player cell

重要提示: 你用该将这些控件绑定到Table View Cell中, 而不是 View Controller! 你知道, 当你的数据源向Table View 请求一个新的单元格时, Table View 给你的不是原型单元格本身, 而是一个*拷贝*(或者,如果可能,一个之前被回收的单元格)。 这说明,会在任何一个时间,同时有多于一个 PlayerCell 实例存在。 如果你将单元格中的一个Label 连接到控制器的 Outlet上面, 那么多个label的拷贝会尝试使用同样一个outlet。 这很快就会引起麻烦。 (另外,将原型单元格的事件连接到控制器上是没问题的。 如果你的单元格上有自定义按钮或其他控件, 你应该这样做。)

现在,我们已经绑定好了属性, 我们可以简化我们的数据源代码:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	PlayerCell *cell = (PlayerCell *)[tableView 
     dequeueReusableCellWithIdentifier:@"PlayerCell"];
	Player *player = [self.players objectAtIndex:indexPath.row];
	cell.nameLabel.text = player.name;
	cell.gameLabel.text = player.game;
	cell.ratingImageView.image = [self 
      imageForRating:player.rating];
    return cell;
}

这样才对, 我们现在将 dequeueReusableCellWithIdentifier 得到的对象转换成了 PlayerCell, 然后我们可以直接使用这里的 Label 和 Image View. 我确实很喜欢原型单元格的使用方式, 这让Table View 少了很多垃圾代码。

你还需要导入 PlayerCell 类:

#import "PlayerCell.h"

运行应用。 当你启动应用后, 它看起来和之前一样, 但在这一切的后面, 我们正在使用我们自定义的单元格。

这里有一些设计上的技巧。 当你在设计自己的单元格时, 这里有一些事情需要注意一下。 首先, 你应该设置 Label 的高亮颜色, 这样当用户点击单元格式,他们看起来会比较好。

Selecting the proper highlight color

第二, 你要保证你加入的内容是灵活的, 当单元格的尺寸改变时,里面内容的尺寸会随着它调整。 当你为单元格提供删除和移动能力的时候, 单元格的尺寸就将会改变。 举个例子:

为 PlayersViewController.m 添加如下方法:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath

{

	if (editingStyle == UITableViewCellEditingStyleDelete)

	{

		[se


Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.




网友评论

登录后评论
0/500
评论
悟静
+ 关注