连接Table Cell用户界面到代码
在你能够在table view cell中显示动态数据之前,你需要创建outlet来连接storyboard中的属性和在MealTableViewCell.swift文件中代表table view cell的代码。
连接这些视图到MealTableViewCell.swift代码
- 在storyboard中,选择table view cell中的label。
-
打开助理编辑器。
-
如有必要,尽可能扩展工作区空间。
-
在编辑器选择器栏中,它显示在助理编辑器的顶部,把助理视图从预览视图切换到切换到Automatic > MealTableViewCell.swift.
MealTableViewCell.swift显示在右侧的编辑器中。
- 在MealTableViewCell.swift中,找到class行:
class MealTableViewCell: UITableViewCell {
- 在class行的下面,添加下面注释:
//MARK: Properties
-
按住Control键,从画布中拖拽label到右侧编辑器显示的代码中,在刚才添加的注释的下面释放。
-
在弹出的对话框中,Name字段键入nameLabel。
让其他选项保持原样。你的对话框看起来是这样的。
- 点击Connect。
- 在storyboard中,选择table view cell的image view。
-
按住Control键,从画布中拖拽image view到右侧编辑器显示的代码中,在刚才添加的nameLabel属性下面释放。
-
在弹出的对话框中,Name字段键入photoImageView。
让其他选项保持原样。并点击Connect。
- 在storyboard中,选择table view cell的rating控件。
-
按住Control键,从画布中拖拽rating控件到右侧编辑器显示的代码中,在刚才添加的photoImageView属性下面释放。
-
在弹出的对话框中,Name字段键入ratingControl。
让其他选项保持原样。并点击Connect。
在MealTableViewCell.swift中你的outlet看上去应该是这样的:
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var photoImageView: UIImageView!
@IBOutlet weak var ratingControl: RatingControl!
加载初始数据
为了在你的table cell中显示真实数据,你需要编写代码来加载这些数据。在这点上,你已经有菜品的数据模型了:Meal类。你还需要保存这些菜品的一个列表。跟踪这个数据的最自然的地方是与菜品列表场景连接的自定义视图控制器子类。这个视图控制器将管理显示菜品列表的视图,并有一个引用指向在用户界面显示的内容背后的数据模型。
首先,创建一个自定义的table view controller子类来管理这个菜品列表场景。
创建一个UITableViewController子类
- 选择File > New > File (或者按下 Command-N)。
- 在出现的对话框中,选择iOS,并且选择Cocoa Touch Class。
- 点击Next。
- 在Class字段,键入Meal。
- 在Subclass of字段,选择UITableViewController。
把类标题改为MealTableViewController。 - 确保Also create XIB file选项没有被选中。
XIB文件是一个旧的通过视图控制器设计视图管理的方式。它们早于storyboard出现,基本相当于代表storyboard上的单一视图。这个视图控制器不需要一个XIB文件,因为你已经定义它连接应用的storyboard了。 - 确保语言是Swift。
- 点击Next。
保存位置是项目目录。
Group选项为默认的FoodTracker。
在目标区域,你的应用被选择,而应用的tests没有被选择。 - 其他的选项保持不变,点击Create。
Xcode创建了MealTableViewController.swift,一个定义自定义table view controller子类的源代码文件。 - 必要时,在Project navigator,拖拽MealTableViewController.swift到和其他的Swift文件一起。
在这个自定义类中,你能定义一个属性来存储Meal对象的列表。Swift标准(Swift standard library)包含一个被称为Array(数组)的结构,它能够很好的跟踪列表的项。
加载初始数据
-
返回标准编辑器。并尽可能的扩展工作区空间。
- 打开MealTableViewController.swift。
- 紧跟着class行添加下面的代码:
//MARK: Properties
var meals = [Meal]()
这代码在MealTableViewController声明了一个属性,并用一个默认值初始化它(一个空的元素项为Meal对象数组)。声明meals为变量而不是常量,意味着你能在初始化它之后还可以给它添加元素项。
- Table view controller模版包含很多存根方法以及对于这些方法的注释。这些占位的实现方法你可以删除注释和扩展(让它们可用)来定义表的外观和行为。在你设置完成模型数据后你将看到这些方法。现在,滚动到这些方法的下面,在结束花括号的上面添加如下方法:
//MARK: Private Methods
private func loadSampleMeals() {
}
这是一个辅助方法,用来加载样本数据到应用。
- 在loadSampleMeals()方法中,首先加载下面三个菜品图片:
let photo1 = UIImage(named: "meal1")
let photo2 = UIImage(named: "meal2")
let photo3 = UIImage(named: "meal3")
确保图片在项目中的名字和在代码中的一致。
- 在加载完这些图片后,创建三个菜品对象。
guard let meal1 = Meal(name: "Caprese Salad", photo: photo1, rating: 4) else {
fatalError("Unable to instantiate meal1")
}
guard let meal2 = Meal(name: "Chicken and Potatoes", photo: photo2, rating: 5) else {
fatalError("Unable to instantiate meal2")
}
guard let meal3 = Meal(name: "Pasta with Meatballs", photo: photo3, rating: 3) else {
fatalError("Unable to instantiate meal2")
}
因为菜品类的 init!(name:, photo:, rating:)初始化器是可失败的,所以你需要检查初始化器返回的结果。在本例中,你传递的是有效的参数吗所以初始化器永远不会失败。若果初始化器失败,你的代码就有了错误。为了帮助你标记和修复错误,如果初始化器失败,fatalError()函数就会在控制台打印一个错误消息,并且应用程序终止。
- 在创建Meal对象之后,使用如下方法添加它们到meals数组。
meals += [meal1, meal2, meal3]
- 找到 viewDidLoad()方法。模版的实现看上去是这样的:
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
模版实现的这个方法包含注释,这些注释是在Xcode创建MealTableViewController.swift的时候插入的。像这样的代码注释在源代码文件中提供了提示和上下文信息,但是在本课中你用不到它们。
- 在 viewDidLoad()方法中,删除注释,并在super.viewDidLoad()后面添加如下方法来加载样本菜品数据。
// Load the sample data.
loadSampleMeals()
当视图加载时,这个代码调用你刚写的加载样本数据的辅助方法。你把它分离到自己的方法中,为的是代码更加模块化和易读。
你的viewDidLoad()方法看上去应该是这样的:
override func viewDidLoad() {
super.viewDidLoad()
// Load the sample data.
loadSampleMeals()
}]
你的loadSampleMeals()方法看上去应该是这样的:
private func loadSampleMeals() {
let photo1 = UIImage(named: "meal1")
let photo2 = UIImage(named: "meal2")
let photo3 = UIImage(named: "meal3")
guard let meal1 = Meal(name: "Caprese Salad", photo: photo1, rating: 4) else {
fatalError("Unable to instantiate meal1")
}
guard let meal2 = Meal(name: "Chicken and Potatoes", photo: photo2, rating: 5) else {
fatalError("Unable to instantiate meal2")
}
guard let meal3 = Meal(name: "Pasta with Meatballs", photo: photo3, rating: 3) else {
fatalError("Unable to instantiate meal2")
}
meals += [meal1, meal2, meal3]
}
检查点:通过 Product > Build.选择构建项目。你的构建应该时没有错误的。注意,这时候,你或许看到一个关于在应用中无法到达View Controller场景的Xcode警告。你将在下一课修复它。在本课剩下来的部分,先忽略它。
重要
如果你运行出现问题,确保项目中的图片名字是否真的和在代码中使用的名字一致,
显示数据
现在,你的自定义table view controller子类,MealTableViewController,有了一个可变数组,它用一些样本数据预先做了填充。现在你需要在用户界面上显示这些数据。
为了显示动态数据,一个table view需要两个重要的帮手:数据源(data source)和委托(delegate)。一个table view 数据源,就像它的名字暗示的那样,提供这个table view要显示的数据。一个table view委托帮助table view管理cell的选择、行高、以及与显示数据相关的其他方面。默认情况下,UITableViewController及其子类采用必要的协议来让table view controller成为它关联的表视图的数据源(UITableViewDataSource协议)和委托(UITableViewDelegate协议)。你的工作就是在table view controller子类中实现合适的协议方法,这样你的table view就有了正确的行为。
Table view需要实现三个表视图数据源方法。
func numberOfSections(in tableView: UITableView) -> Int
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
第一个 numberOfSections(In:)方法,它高速表视图有多少section(部分)要显示。section是表视图内部cell的可视化分组,它在表视图里有很多数据的时候特别有用。对于简单的表视图,比如FoodTracker 应用,你只需要一个部分显示就可以了,所以实现这个方法很简单。
在table view 中显示一个section
- 在 MealTableViewController.swift中,找到numberOfSections(In:)数据源方法。模版实现看上去是这样的:
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 0
}
- 把返回值改为1,删掉警告注释。
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
这个代码让table view显示1个部分。你删掉的注释说的是#warning Incomplete implementation(警告 没有完成实现),但你已经实现了,所以就删掉了。
接下来的数据源方法,tableView(_:numberOfRowsInSection:),高速表视图一个给定的部分里面有多少行。你的表视图只有一个部分,并且每个Meal对象都应该有自己的行。这就意味着行数应该是meals数组里Meal对象的数目。
返回table view的行数
- 在MealTableViewController.swift中,找到tableView(_:numberOfRowsInSection:)数据源方法。它的模版实现看上去是这样的:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 0
}
你要返回你有的菜品树木。Array有一个属性称为count,它返回数组中项目的总数,所以行数就是meals.count。
- 改变tableView(_:numberOfRowsInSection:)数据源方法返回合适的行树,移除警告注释。
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return meals.count
}
最后一个数据源方法,tableView(_:cellForRowAt:),为给定的行配置并提供一个cell用来显示。表视图中的每个行都有一个cell,这个cell决定行中显示的内容以及这些内容如何布局。
对于只有少量行的表视图,所有行或许都在屏幕上,所以这个方法调用表中的每一行。但是有很多行的表视图,在给定的时间里只有小部分能够显示在屏幕上。表视图如果只请求需要被显示的行的cell就会大大提高了效率,这就是tableView(_:cellForRowAt:)允许表视图做的。
对于在table view中任何给定的row,你通过获取在meals数组中的合适的Meal来配置cell,然后用Meal类的合适值来设置cell的属性。
在table view中配置和显示cell
- 在MealTableViewController.swift,找到tableView(_:cellForRowAt:) 数据源方法并取消注释。(要取消注释,只要删除围绕它的 /* 和 */字符。)
完成后这个这个方法看上去是这样的:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
// Configure the cell...
return cell
}
dequeueReusableCell(withIdentifier:for:)方法从这个table view中请求一个cell。作为替代在用户滚动cell的时候创建新cell并删除旧cell的方式,表格会尽可能的重用cell。如果没有可用的cell, dequeueReusableCell(withIdentifier:for:)会实例化一个新的;但是如果cell滚动到屏幕的外面,它们会被重用。标识符(identifier)会告诉dequeueReusableCell(withIdentifier:for:)哪个类型的cell要被创建或重用。
为了这段代码能够工作,你需要改变在storyboard中的cell(MealTableViewCell)标识符属性,然后添加代码来配置cell。
- 在这个方法一开始的地方添加下面的代码:
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "MealTableViewCell"
这使用storyboard中设置的标识符创建了一个常量。
- 使用cellIdentifier变量更新方法中的标识符,像下面这样:
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
- 因为你创建了一一个你想用的自定义cell类,将cell的类型降级到你自定义的cell的子类,MealTableViewCell。
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? MealTableViewCell else {
fatalError("The dequeued cell is not an instance of MealTableViewCell.")
}
这段代码有很多事要做:
- as? MealTableViewCell表达式试图把返回对象从UITableViewCell类降级到MealTableViewCell类。这个返回值是一个可选值(optional)。
- guard let表达式会安全解包这个可选值。
- 如果你的storyboard设置正确,并且cellIdentifier匹配storyboard的标识符,那么降级处理将不会失败。如果降级失败,fatalError()函数就会在控制台打印错误信息,并终止应用。
- 在guard语句后面,添加下面的代码:
// Fetches the appropriate meal for the data source layout.
let meal = meals[indexPath.row]
这个代码从meals数组获取合适的菜品对象。
- 现在,使用meal对象来配置你的cell。用下面的代码来替换// Configure the cell注释。
cell.nameLabel.text = meal.name
cell.photoImageView.image = meal.photo
cell.ratingControl.rating = meal.rating
这个代码为每个在table view cell中的视图设置合适的数据,这些数据来自meal对象。
你的tableView(_:cellForRowAt:)方法看起来是这样的:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "MealTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? MealTableViewCell else {
fatalError("The dequeued cell is not an instance of MealTableViewCell.")
}
// Fetches the appropriate meal for the data source layout.
let meal = meals[indexPath.row]
cell.nameLabel.text = meal.name
cell.photoImageView.image = meal.photo
cell.ratingControl.rating = meal.rating
return cell
}
在用户界面显示数据的最后一步是把MealTableViewController.swift中定义代码连接到菜品列表场景。
将Table View Controller指向MealTableViewController.swift
- 打开storyboard。
-
通过点击在场景dock直到整个场景有了一个蓝色的轮廓来选择table view controller。
- 打开Identity inspector。
-
在Identity inspector中,找到Class字段,并选择MealTableViewController。
检查点:运行应用。你在 viewDidLoad()方法中添加的项目列表应该显示在table view的cell上了。你可能注意到在table view cell和状态栏之间有一个小的重叠——你将在下一课修复它。
为导航准备菜品详情场景
当你准备在FoodTracker应用中实现导航,你需要删除一些占位的代码和你不再需要的的用户界面。
清除项目不使用的部分
-
打开storyboard并查看菜品详情场景。
你的菜品详情场景的用户界面看上去是这样的:
-
在这个场景中,选择Meal Name 标签(label),并按下删除键删除它。
在栈视图中的其他元素会自动重定位。
- 打开ViewController.swift。
- 在ViewController.swift中,找到textFieldDidEndEditing(_:)方法。
func textFieldDidEndEditing(_ textField: UITextField) {
mealNameLabel.text = textField.text
}
- 删除设置label的text属性的行。
mealNameLabel.text = textField.text
你将很快使用新的实现来替换它。
- 在ViewController.swift,找到mealNameLabel的outlet,并删除它。
@IBOutlet weak var mealNameLabel: UILabel!
因为现在你有两个视图控制器在项目中,需要给ViewController.swift一个更加有意义的名字。
重命名ViewController.swift文件
- 在project navigator,点击 ViewController.swift文件一次,并按下回车键。
Xcode会允许你为这个文件输入一个新名字。 - 重命名为MealViewController.swift,按下回车键。
- 在MealViewController.swift中,找到类声明行
class ViewController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
- 改变类名为MealViewController
class MealViewController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
- 在文件顶部的注释中,也把名字从ViewController.swift 改为MealViewController.swift。
- 打开storyboard。
-
通过点击它的场景dock选择视图控制器。
- 选中这个视图控制器,打开Identity inspector。
-
在Identity inspector,在Class字段把ViewController改为MealViewController。
检查点:构建或运行应用。一切应该如常。
小结
在本课中,你构建了一个自定义的table view cell。你把模型对象附加到了table view controller。你给模型添加了样本数据,并且你实现使用模型数据动态的填充表格所需的表视图控制器代码。
下一课,你将添加在表视图和菜品视图之间导航的功能。
注意
想看本课的完整代码,下载这个文件并在Xcode中打开。
下载文件