Swift语言Storyboard教程:第一部分

本文由CocoaChina翻译小组@TurtleFromMars翻译自:Storyboards Tutorial in Swift: Part 1

更新记录: 该Storyboard教程由Caroline Begbie更新iOS 8和Swift相关内容。原文作者为教程编纂组的成员Matthijs Hollemans

2014/12/10更新: 更新至Xcode 6.1.1。

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

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

The full storyboard for the app

或许你现在并不清楚这个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 模板:

01_sb_newproject

如下填写模板选项:

  • Product Name: Ratings

  • Organization Name: 随意填写

  • Company Identifier: 你的App使用的标识符,逆域名记法

  • Language: Swift

  • Devices: iPhone

  • Use Core Data: 不选

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

initial project config

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

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

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

The initial storyboard

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

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

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

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

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

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

Disabling auto layout

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

Disabling size classes

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

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

Dragging controls into storyboard

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

Document outline

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

The document outline button

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

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

Dock

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

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

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

First app in the simulator

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

1
2
3
4
5
6
7
8
9
10
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 ,你会看到这一条:

Info.plist

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

在General分页的Project Settings和Deployment Info中也可以看到:

Project Settings

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

添加分页标签

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

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

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

Tab bar controller

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

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

Container Relationship

注:如果你想一起移动分页栏控制器和附带的视图控制器的话,先缩小画面,然后按住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 。

Set Initial View Controller

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

Set Initial View Controller

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

Is Initial View Controller

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

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

App-with-tabs

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

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

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

添加表视图控制器

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

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

Table view controller

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

Navigation controller

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

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

Relationship arrow

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

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

Simulated metrics

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

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

Relationship Segue

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

Relationship arrow

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

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

Drag tab items

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

First tab with table view

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

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

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

Rename

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

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

Copy Resources

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

Players Image

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

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

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

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

App with tab bar images

原型表项(Prototype Cell)

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

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

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

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

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

Cell setup

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

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

Players view controller

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

Custom class

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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中追加以下代码:

1
2
3
4
5
//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下面添加一个玩家数组属性,用来保存玩家列表:

1
var  players: [Player] = playersData

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


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

1
2
3
4
5
6
7
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
   return  1
}
  
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return  players.count
}

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

1
2
3
4
5
6
7
8
9
10
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 with players

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

注:该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。原型表项的最终设计大概是这个样子:

Custom cell design

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}

讲解一下刚才做的工作:

  1. dequeueReusableCellWithIdentifier在回收表项可重用的情况下会抽出重用标识符为PlayerCell的表项,否则创建一个新表项。

  2. 按行号查看Player对象并将其赋值给player

  3. 按表项上的tag找到label和图片,并参照player对象填充数据。

应该可以了。现在再次运行App,大概会像这样:

Wrong table cell height

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

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

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

Right table cell height

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

App with proper row height

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

使用表项的子类

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

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

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

1
2
3
@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执行同样操作。

Connect outlet

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

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

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

IBOutlet connection

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

1
2
3
4
5
6
7
8
9
10
11
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试试看。看起来应该和之前一样,但在幕后,现在使用的已经是你自己的表项子类了!

何去何从?

点此链接 下载本项目进行到现在的所有源代码。

继续阅读本教程的第二部分,内容包含segue(转场),static table view cell(静态表项),“添加玩家”页面,“游戏选择”页面,还有本教程完整示例项目的下载!

如果在本教程的中对某些问题不清楚,你可以阅读我们的iOS学徒系列来夯实基础,该系列中你会从头开始开始学习iOS开发者应该具备的基础知识,极为适合初学者入门,同样适用于查缺补漏。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值