Storyboard使用教程一

Storyboard是最先在iOS 5引入的一项振奋人心的特性,大幅缩减构建App用户界面所需的时间。

要介绍Storyboard是什么,我打算从这张图讲起。下面是您将会在本教程中构建的Storyboard:


image

或许你现在并不清楚这个App是用来做什么的,但其中有哪些页面,还有页面间的关联都一目了然。这就是使用Storyboard的力量。

如果App中包括很多不同的页面,使用Storyboard可以帮你减少实现页面间跳转的胶合代码。过去的开发者对应每个视图控制器分别创建界面设计文件(即“nib”或“xib”文件),现在,只要一个Storyboard就可以包揽所有视图控制器的界面设计和他们之间的关联。

Storyboard有很多优点:

  • 使用Storyboard可以更好地了解App中所有的视图以及它们之间的关联的概况。掌控全局更加容易,因为所有的设计都包含在一个文件中,而不是分散在很多单独的nib文件中。
  • Storyboard可以描述不同视图之间的过渡,这种过渡叫做“segue”(译注:意为“转场”,而“Storyboard”原意为“分镜”,均源自电影术语),你可以直接在Storyboard中通过连接不同的视图控制器来创建转场。多亏有了转场,打理界面的代码比以前要少了。
  • Storyboard通过新的原型表项(prototype cell)和静态表项(static cell)特性,让处理表视图(table view)的工作更加轻松。几乎完全可以在Storyboard编辑器里搞定表视图的设计,同样也减少了代码量。
  • Storyboard使自动布局(Auto Layout)更易用。自动布局功能可以让你通过界面元素之间的数学关系定义来确定元素的位置和尺寸,极大简化了不同尺寸屏幕的适配工作。自动布局不在本教程范围之内,若想了解更多,请参阅自动布局入门。
    如果你非常讨厌Interface Builder,或者推崇用代码搞定所有界面的话,Storyboard可能不适合你。个人主张是代码能少写就少写,特别是UI代码,所以Storyboard简直就是为我准备的一把利器。

如果你想继续使用nib,那就继续用吧,要知道Storyboard里是可以使用nib的,两者并非互斥关系。

本教程中,你会了解Storyboard可以做什么,我们将构建一个简单的App,功能大致是创建玩家列表和游戏列表,然后给玩家技能评分。过程中你会学到大多数可以用Storyboard完成的最常见的任务。

准备开始

打开Xcode,创建新项目。选用 Single View Application 模板:


image

如下填写模板选项:

  • Product Name: Ratings
  • Organization Name: 随意填写
  • Company Identifier: 你的App使用的标识符,逆域名记法
  • Language: Swift
  • Devices: iPhone
  • Use Core Data: 不选

项目创建完成后,Xcode的主界面应该如下图所示:


image

这个新项目包含2个类:AppDelegate 和 ViewController, 此外还有本教程的主角: Main.storyboard 文件。

这是一个只支持竖屏显示的App,所以在继续之前,在项目综合设置上面看到的 Deployment Info - Device Orientation下面把 Landscape Left和Landscape Right 选项勾掉。

接下来我们看一下Storyboard,点击项目浏览器中的 Main.storyboard 就可以在Interface Builder中打开。

一个视图控制器在Storyboard中的官方术语是“场景(scene)”,但这两种叫法是相通的。一个视图控制器在Storyboard中可以叫做场景。

这里可以看到一个包含空视图的视图控制器。在这个视图控制器左边指向它的箭头表明它是这个Storyboard中要显示的第一个视图控制器。

在Storyboard编辑器中设计布局的方法是从右下角的Object Library(对象库)中把控件拖入视图控制器,非常容易。

注:你会注意到默认场景是一个正方形。Xcode 6默认为Storyboard和nib文件开启自动布局(Auto Layout)和尺寸归类(Size Classes)。自动布局和尺寸归类这两项新技术可以构建易于调整大小的用户界面,这对支持不同尺寸的iPhone和iPad非常有用。

自动布局由iOS 6引入,尺寸归类由iOS 8引入。两者都需要一定的学习曲线,所以本教程中暂不使用,但为了支持不同的设备尺寸,以后还是要接触到的。

在继续探索之前,先在当前Storyboard的 File inspector(文件检查器) 中禁用Auto Layout和Size Classes,如图:

Xcode询问操作时,选择保留 iPhone 的尺寸归类数据,然后点击 Disable Size Classes :

现在,场景变成了4英寸iPhone尺寸的样子。

从右下方的对象库里把一些控件拖到空的视图控制器上,感受一下Storyboard编辑器的工作方式:

控件拖进来之后应该会在左边的文档大纲(Document Outline)中显示:

如果没看到文档大纲,请点击Storyboard面板左下角的这个按钮:

Storyboard显示所有视图控制器的内容,当前的Storyboard中仅有一个视图控制器(场景),在本教程后面我们会添加其他场景。

在场景上面还有一个缩小的文档大纲,称作Dock:

Dock显示场景中最上层的对象,每个视图都至少有一个 视图控制器(View Controller) 对象,一个 第一响应者(First Responder) 对象,一个 出口(Exit) 项。除此之外也可以有其他的最上层对象。Dock方便连接outlet和action,当你想把某个对象连接到视图控制器时,只需把它拖到Dock的图标上。

注:你可能不常用到First Responder。这是指任意对象在任意时间具有第一响应状态的代理对象。举个例子,把一个按钮的Touch Up Inside事件拖到First Responder的 cut: 选择器上。如果在某时有一个文本字段具有输入焦点,此时按下该按钮,就可以让该文本字段,也就是现在的第一响应者,把其中的文本剪切到剪贴板。

运行App,它看起来应该和你在编辑器中设计的样子相同(截图可能与你的不同,仅供演示参考,教程后面不会用到):

你定义的这个视图控制器被设定为初始视图控制器,但App是如何加载的呢?答案就在应用代理(application delegate)当中,打开 AppDelegate.swift ,你会看到如下代码:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
 var window: UIWindow?

 func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
// Override point for customization after application launch.
return true
}

上面的 @UIApplicationMain 标记指定这个AppDelegate类为该模块的入口。使用Storyboard时,应用代理必须继承 UIResponder ,必须含有 UIWindow 属性,几乎所有的方法都是空的,甚至 application(_:didFinishLaunchingWithOptions:) 也只是返回true而已。
秘密藏在 Info.plist 文件里,在Supporting Files Group里找到并点击 Info.plist ,你会看到这一条:


Storyboard用 UIMainStoryboardFile
(即Main storyboard file base name键) 来指明App启动时必须加载的Storyboard的名称。当设置生效,UIApplication
会加载对应名称的Storyboard文件,自动将该Storyboard中的初始视图控制器实例化,并将其纳入一个新的 UIWindow
对象中。
在General分页的Project Settings和Deployment Info中也可以看到:

接下来真正开始创建包含多个视图控制器的评分App吧。

添加分页标签

你要构建的这个评分App中含有由分页标签控制的两个视图,使用Storyboard创建分页标签非常容易。

现在这个Storyboard需要从头做起,切回 Main.storyboard 然后把刚才做的视图控制器删掉。在文档大纲中点击 View Controller 并按下delete键即可。

把一个 Tab Bar Controller(分页栏控制器) 从对象库拖到面板中。你可能需要让Xcode最大化,因为分页栏控制器附带两个视图控制器,需要腾出更多空间,你可以双击面板进行缩放,或者按住control点击面板,在弹出的菜单中选择缩放比例。

一个新增的分页栏控制器默认附带两个额外的视图控制器,每个分页标签一个控制器。由于UITabBarController包含一个或多个其他的视图控制器,它被称作 容器视图控制器。此外还有两种常见的容器视图控制器,Navigation Controller(导航控制器)和Split View Controller(分割视图控制器)。

容器关系由分页栏控制器和他所包含的视图控制器之间的箭头表示,如下图这个箭头上的图标表示嵌入关系。

注:如果你想一起移动分页栏控制器和附带的视图控制器的话,先缩小画面,然后按住command点击,或直接拖选多个场景,这样可以同时移动多个场景。(选中的场景轮廓为淡蓝色。)

在第一个视图控制器(当前名称为“Item 1”)中拖入一个Label(文本标签)并将其文本设为 "First Tab",同理,在第二个视图控制器中加入文本为"Second Tab"的Label,这样你就可以看到分页标签切换后的变化。

注:编辑器缩小时无法向场景内拖入控件,此时需要先在面板上双击,回到正常缩放比例。

构建,运行,你会在Console中看到类似信息:

Ratings[18955:1293100] Failed to instantiate the default view controller for UIMainStoryboardFile 'Main' - perhaps the designated entry point is not set?

幸运的是这条报错信息讲得很清楚:未设置入口,也就是刚才删除最先使用的那个场景之后没设置初始视图控制器。为解决问题,选中这个分页栏控制器,然后在 Attributes Inspector(属性检查器) 中选定 Is Initial View Controller 。

注:在Xcode 6.2中,上述选项已被控件取代。先选中当前分页栏控制器,然后从对象库里把一个Storyboard Entry Point(Storyboard入口)拖上去,可以拖到控制器上面,也可以拖入文档大纲。

现在,一开始的那个箭头已经指向当前的分页栏控制器了:

注:Xcode 6.2 beta在这里可能会崩溃,如果出现问题,请选中该分页栏控制器的某个视图控制器,把入口拖上去,然后再把入口箭头拖到分页栏控制器上。

这意味着启动App时, UIApplication 会把此分页栏控制器作为主画面。运行App试一试,现在App下面有分页标签栏了,可以用分页标签在两个视图控制器之间切换。

提示:你也可以通过拖拽视图控制器之间的箭头来改变初始视图控制器。

其实在前面你也可以选用Xcode自带的标签分页式App模板(即Tabbed Application模板)创建App,但最好还是了解一下工作原理,以后有必要的时候也能手动创建分页栏控制器。

注:如果在分页栏控制器上连接超过5个场景,App在运行时会自动将其归入一个More分页标签,干净利落。

添加表视图控制器

现在附属于分页栏控制器的两个场景都是标准UIViewController实例,接下来你会把其中第一个分页标签对应的场景替换为UITableViewController。

在文档大纲中点选第一个视图控制器并将其删除,然后从对象库中把一个新的 Table View Controller(表视图控制器)拖到原场景所在的地方。

接下来把表视图控制器放到导航控制器(Navigation Controller)中,选中表视图控制器,在Xcode菜单中选择 EditorEmbed InNavigation Controller ,现在面板中又加入了另一个控制器:

你也可以从对象库中拖入导航控制器后再嵌入表视图,但这个操作一般来讲使用菜单命令会更省时。

与分页栏控制器类似,导航控制器也是容器视图控制器,所以有一个关系箭头指向表视图控制器,你也可以在文档大纲中看到这个关系:

注意,嵌入表视图控制器后,Interface Builder自动给它添加了一个导航栏,因为当前视图是在导航控制器的框架中显示的。它并不是实际存在的UINavigationBar对象,只是模拟显示情况。

打开表视图控制器的属性检查器,上面可以看到 Simulated Metrics(模拟度量)选项:

Storyboard中的默认值为“Inferred(推断)”,意思是该场景在处于导航控制器中时会显示导航栏,处于分页栏控制器中时会显示分页栏等等。你可以修改这些设置,但是请记住,这只是方便你设计界面时参考的模拟显示,并不会在运行时使用,仅仅是视觉设计的辅助工具,用来表示视图最后应该是什么样子。

接下来把这两个新场景连接到分页栏控制器,按住control从分页栏控制器拖到导航控制器,松手时会弹出一个小选单,选择 Relationship Segue – view controllers 选项:

这会在两个场景间新建一个关系箭头,与分页栏控制器包含控制器一样,都是嵌入关系。

分页栏控制器有两个嵌入关系,分别对应两个分页标签。导航控制器上有一个表视图控制器的嵌入关系。

创建这个连接后,分页栏控制器中会添加一个新分页标签,默认名称为“Item”。在这个App中,你希望第一个分页标签对应这个新场景,直接拖动分页标签,更改顺序:

运行App试试看,现在第一个分页标签中包含一个嵌入在导航控制器中的表视图。

在添加实际功能之前,你还需要再修整一下Storyboard,将第一个分页标签命名为"Players",第二个命名为"Gestures"。不是在分页栏控制器上修改,而是在这些分页标签对应的视图控制器上修改。

将一个视图控制器连接到分页栏控制器后,在场景下面和文档大纲中会看到它被赋予的 分页栏项(Tab Bar Item) 对象,可以用来设置分页标签的标题和在分页栏控制器中看到的图标。

选中导航控制器中的分页栏项,在属性检查器中将标题设为Players:

以同样的方法把第二个分页标签对应场景栏目改名为Gestures。

一个设计精良的App应该为分页标签附上图标。教程资源中有个Image文件夹,把这个文件夹拖入项目,选择“Copy items if needed”并点击Finish:

在Players分页栏项的属性检查器中选择图片 Players.png 。

你可能已经想到了,给Gestures选择 Gestures.png 。

嵌入导航控制器的一个视图控制器包含用于设置导航栏的 Navigation Item(导航项) 。在文档大纲中选择表视图控制器的导航项,在属性检查器中把Title改成Players。

或者你也可以双击导航栏直接修改title,注意你需要双击的是表视图控制器中的模拟导航栏,而不是导航控制器中的那个导航栏对象。

运行App,欣赏一下这漂亮的分页标签栏吧!一行代码也不用写哦!

原型表项(Prototype Cell)

原型表项允许你直接在Storyboard编辑器中为表视图设计自定义布局。

表视图控制器默认会带一个空的原型表项。点击它,在属性检查器中设置Style为 Subtitle(副标题)。这会立即改变表项的外观,使其包含两个Label。

Storyboard上可以堆叠很多内容,有时可能很难点击到你想选中的东西。如果遇到困难,有几种选择:第一是在面板左侧的文档大纲中选择,第二是快捷键(按住control+option+shift,点击想选择的区域后会弹出指针所指区域的所有元素),第三种选择是Xcode 6的新功能,反复点击可以在各层之间循环。

如果你之前用过表视图,还手动创建过自己的表项,你可能会将其认作UITableViewCellStyle.Subtitle样式。有了原型表项,你可以像刚才那样选择系统内建的样式,也可以自定义设计(我们稍后就要创建了)。

设置Accessory(附件,即表项右侧的附属元素)属性为 Disclosure Indicator(展开方向标,即右键头),并在 Identifier(标识符) 字段中输入 PlayerCell。所有的原型表项仍然是标准UITableViewCell对象,所以它们需要一个以供重用的标识符。

运行应用……什么都没变。这没什么值得奇怪的,接下来你还需要为这个表指定一个data source(数据源),这样它才会知道要显示什么。

在项目中添加一个新文件,选择iOS/Source下的Cocoa Touch Class模板,命名为 PlayersViewController ,并确保它是UITableViewController的子类。不要选中Also create XIB file选项,因为你已经在Storyboard中设计好了,今天不用nib!选择Swift语言,点击Next,然后点击Create。

回到Storyboard,选择表视图控制器(确保你选择的是视图控制器而不是其中包含的某个视图)。在身份检查器(Identity inspector)中设置它的 Class 为 PlayersViewController。这对于在Storyboard场景中使用自定义视图控制器的子类很重要,因为如果你不这么做,你的类就都不会被使用!

此后运行App时Storyboard中加载的那个表视图控制器就是PlayersViewController类的实例。

这个表视图要显示玩家列表,所以你需要为App创建主要的数据模型:一个包含Player对象的数组。由iOS/Source下的Swift File模板添加新文件,命名为Player。

在Player.swift中追加以下代码:

import UIKit

class Player: NSObject {
 var name: String
 var game: String
 var rating: Int

init(name: String, game: String, rating: Int) {
self.name = name
self.game = game
self.rating = rating
super.init()
 }
}

没什么特别的东西,Player只是容器对象,其中包含三个属性:玩家名称,进行的游戏,还有1到5星之间的评分。

接下来要创建一组Player测试对象,并在PlayersViewController中赋值到一个数组。请使用Swift File模板创建名为SampleData的新文件,并在SampleData.swift中追加以下代码:

//Set up sample data

let playersData = [ Player(name:"Bill Evans", game:"Tic-Tac-Toe", rating: 4),
 Player(name: "Oscar Peterson", game: "Spin the Bottle", rating: 5),
 Player(name: "Dave Brubeck", game: "Texas Hold 'em Poker", rating: 2) ]    

这里定义了一个叫做playersData的常量,并把写定的Player对象数组赋值给它。

现在在PlayersViewController.swift的class PlayersTableViewController: UITableViewController下面添加一个玩家数组属性,用来保存玩家列表:

 var players: [Player] = playersData     

这里,你可能会在PlayersViewController中定义players变量时顺带就把示例数据准备好了,但以后数据可能源自plist或SQL文件,所以,在视图控制器之外处理数据加载问题是明智之选。

现在你有一个包含多个Player对象的数组,可以在PlayersViewController中绑定数据源了。还是在PlayersViewController.swift中,用以下代码替换表视图数据源方法:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
 return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return players.count
}

实际工作在cellForRowAtIndexPath中。用以下代码替换方法(原来的注释掉):

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell {
 let cell = tableView.dequeueReusableCellWithIdentifier("PlayerCell", forIndexPath: indexPath)
as UITableViewCell

let player = players[indexPath.row] as Player
 cell.textLabel?.text = player.name
 cell.detailTextLabel?.text = player.game
 return cell
}

dequeueReusableCellWithIdentifier(_:forIndexPath:)方法用来检查是否存在可重用的表项。如果没有,就返回一个自动分配的原型表项。你只需要提供之前在Storyboard编辑器中给原型表项设定的重用标识符,本例中对应PlayerCell。一定要设置标识符,否则无法正常工作!

运行App,现在表视图中有玩家项了!

只要写几行代码就可以使用原型表项,赞!

注:该App中只使用了一个原型表项,但如果你的列表需要显示不同种类的表项,你可以向Storyboard中另外添加原型表项。可以复制现有的表项再进行修改,也可以增大表视图的Prototype Cells属性值。记得每个表项都要设置自己的重用标识符。

设计自己的原型表项

对很多App来说使用内建的标准表项样式已经足够了,但这个App需要在表项的右侧添加一个显示评分(1星到5星)的图片。标准表项样式不支持在这里包含图片视图,所以你只能自己创建自定义设计。

切回Main.storyboard,选择表视图中的原型表项,在属性检查器中设置Style属性为Custom(自定义),随后默认的Label不见了。

首先让表项增高一些,拖动底边上的小方块或在尺寸检查器(Size inspector)中修改Row Height(行高)值,设置表项高度为55点(points)。

从Objects Library拖两个Label到表项上,把它们放到和之前的标准样式差不多的地方,你可以在属性检查器中随意设置字体和颜色。设置上面的Label文本为“Name”,下面的为“Game”。

把一个Image View(图片视图)拖到表项中,放在右面紧挨展开方向标的地方,设宽度为81点,高度不是很重要。将其Mode设为Center(在属性检查器的View下面),保证载入视图的图片不会被拉伸。

在尺寸检查器中设Label宽度为190点。Label不应盖住Image View。原型表项的最终设计大概是这个样子:

因为这是一个自定义表项,所以再也不能用 UITableViewCell中的textLabel和detailTextLabel属性来设置文本了。这些属性只在标准表项类型中有效,它们指向的label在该表项中已经不存在了。为此,你需要用tag(标记)找到相应的label。

你也可以选择创建一个继承UITableViewCell的自定义类并包含对应表项视图中的label的属性。而tag可以用来简化工作,在简单情况下是很不错的解决方案。不过本教程后面会尝试使用自定义类的方法。

在属性检查器中设置“Name”Label的tag值为100,“Game”Label为101,Image View为102.

打开PlayersViewController.swift,在后面如下添加新方法imageForRating;

func imageForRating(rating:Int) -> UIImage? {
 switch rating {
 case 1:
return UIImage(named: "1StarSmall")
 case 2:
return UIImage(named: "2StarsSmall")
case 3:
return UIImage(named: "3StarsSmall")
 case 4:
return UIImage(named: "4StarsSmall")
 case 5:
return UIImage(named: "5StarsSmall")
 default:
return nil
    }
}

很简单,该方法根据评分返回不同的星级图片。依然在PlayersViewController中,如下修改tableView(_:cellForRowAtIndexPath:)方法:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("PlayerCell", forIndexPath: indexPath) as UITableViewCell //1

let player = players[indexPath.row] as Player //2

if let nameLabel = cell.viewWithTag(100) as? UILabel { //3
nameLabel.text = player.name
 }
 if let gameLabel = cell.viewWithTag(101) as? UILabel {
gameLabel.text = player.game
 }
 if let ratingImageView = cell.viewWithTag(102) as? UIImageView {
ratingImageView.image = self.imageForRating(player.rating)
}
return cell
}

讲解一下刚才做的工作:

dequeueReusableCellWithIdentifier在回收表项可重用的情况下会抽出重用标识符为PlayerCell的表项,否则创建一个新表项。
按行号查看Player对象并将其赋值给player。
按表项上的tag找到label和图片,并参照player对象填充数据。
应该可以了。现在再次运行App,大概会像这样:

嗯,看起来不大对劲,表项都重叠在一起了。你只修改了原型表项的高度,但是并没有把表视图考虑进去。这里有两个解决方案,一是改变表视图的Row Height属性,二是实现tableView(tableView:heightForRowAtIndexPath:)方法。本例中前者更合适,因为只有一种表项,而且我们已经事先了解表项的高度。

注:如果无法事先判定表项的高度,或者各行的高度可能不一致,可以使用tableView(tableView:heightForRowAtIndexPath:)方法。

回到Main.storyboard,在表视图的尺寸检查器中设Row Height为55点:

现在运行,看起来好多了!

哦,还有一点,如果之前修改表项高度时没有手动输入数据,而是拖动表项边上的小方块的话,表视图的行高属性也会自动随之改变。所以在构建过程中你可能并没碰到上述问题。

使用表项的子类

这个表视图用起来已经相当不错了,但我不大喜欢用tag来获取原型表项的子视图。如果可能的话,把这些label于outlet连接并使用相应属性要优雅得多。事实是可行的。

在项目中以Cocoa Touch Class模板添加一个新文件,命名为PlayerCell并令其继承UITableViewCell。不要选中创建XIB的选项,因为Storyboard里已经有表项了。

在PlayerCell类的类定义下面添加以下属性

@IBOutlet weak var gameLabel: UILabel!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var ratingImageView: UIImageView!

这些变量都是IBOutlet,它们可以在Storyboard中与场景建立连接。

回到Main.storyboard,选中原型表项PlayerCell,并在身份检查器中把它的class改成PlayerCell。现在每当通过dequeueReusableCellWithIdentifier(_:forIndexPath:)向表视图请求一个新表项时,它会返回PlayerCell实例而不是UITableViewCell。

注意:这里我们把类名跟重用标识符设置成一样了,都是PlayerCell。这只是因为个人喜欢保持一致,类名跟重用标识符毫不相干,如果你愿意,也可以起不同的名字。

下面令label以及image view与outlet连接。在Storyboard中切到连接检查器(Connections inspector),然后在面板或文档大纲中选择Player Cell,把连接检查器中的nameLabel outlet拖到Name label对象上。对gameLabel和ratingImageView执行同样操作。

重点:控件要连接的是表项,而不是视图控制器!当你的数据源向表视图通过dequeueReusableCellWithIdentifier索求一个新表项的时候,表视图并不是把原型表项交给你,而是复制一份给你(或是之前被纳入回收空间的一个已有表项)。

这就意味着在同一时间不止有一个PlayerCell的实例,如果把表项中的label连接到了视图控制器的outlet上,不同的label拷贝会试图使用同一个outlet,这是自找麻烦。(另一方面,把原型表项连接到视图控制器的action上是可行的,当你的表项中含有自定义按钮或者是其他UIControl时可能会用到。)

除使用连接检查器之外,你也可以按住control从PlayerCell拖到控件上,然后在弹出的选单中选择outlet名称。

现在已经绑定属性,可以稍微简化数据源的代码。在PlayersViewController中如下修改tableView(_:cellForRowAtIndexPath:)方法:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
 -> UITableViewCell {
     let cell = tableView.dequeueReusableCellWithIdentifier("PlayerCell", forIndexPath: indexPath)
as PlayerCell

let player = players[indexPath.row] as Player
 cell.nameLabel.text = player.name
 cell.gameLabel.text = player.game
 cell.ratingImageView.image = imageForRating(player.rating)
return cell
}

这就更像样了。把从dequeueReusableCellWithIdentifier接收的对象转为一个PlayerCell,然后就可以使用连接到label和图片视图的属性。这样使用原型表项,表视图不像以前那么乱了。

运行App试试看。看起来应该和之前一样,但在幕后,现在使用的已经是你自己的表项子类了!

本文参考来源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 在本系列Storyboard教程的第一部分,我们已经学习了如何使用Interface Builder创建并连接不同...
    余一波_Bobby阅读 12,726评论 2 11
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,022评论 4 62
  • 耳畔传来笃笃的声音 是破晓时布谷鸟啼叫野旷的伐木丁丁 是薄暮里溪边农妇浣洗捣衣的浅唱低吟 是深山禅寺敲醒万籁的木鱼...
    原朔阅读 358评论 0 4
  • hi~小满生活公会又和大家见面啦!不知不觉已经陪伴大家这么久的时间了,这次小满为大家带来了我们新的栏目——小满星座...
    小满生活公会阅读 787评论 0 0
  • 文|枪枪 离家三百米外有一间小酒馆,夜里八点之后,小舞台上都有戏剧表演,主要演希腊神话故事。 我习惯叫一杯甘...
    0378e3b67796阅读 422评论 3 0