iOS Apprentice中文版-从0开始学iOS开发-第四十
The Locations tab
你已经创建了数据模型,并赋予app将新的位置信息保存到数据存储的能力。 现在,你将在第二个标签页中的table view中显示这些位置。
打开storyboard,删除Second Scene。这是工程模版中自带的多余的一个界面,你不需要它。
然后拖拽一个Navigation Controller到画布中。(它会自带一个table view,这才是你需要的界面)
按住ctrl将Tab Bar Controller拖拽到新增的这个Navigation Controller上,并且在弹出的菜单中选择Relationship Segue分节下的view controllers。这样就把这个Navigation Controller添加到Tab Bar Controller中了。
现在Navigation Controller具备一个Tab Bar Item,叫做“Item”,将其重命名为Locations。
双击Root View Controller中的navigation bar,将其标题重命名为Locations。(也可以在Navigation Item的属性检查器中实现这个目的)
打开Root View Controller的身份检查器,将table view controller的Class设置为LocationsViewController,目前这个类还不存在,稍后你会创建它。
此时,整个storyboard看起来会是这个样子:
运行app,并且选择Locations子标签。它看起来还是很苍白的一片,没有什么实际内容:
在你在这个table view中展示数据之前,你首先要设计好它的prototype cell。
选中prototype cell,在属性检查器中设置identifier为LocationCell。
拖拽两个标签到cell中。上面的标签内容写上Description,下面的一个写上Address。这样你就很清楚的明白它们的作用了。
将Description标签的字体设置为System Bold,大小17.将标签的tag设置为100。
将Address标签的字体设置为System,大小14.设置文本颜色为黑色,透明度50%,将标签的tag设置为101。
此时cell看起来应该是这个样子:
确保两个标签的宽度覆盖整个cell。
仅仅是改变cell的高度是不够的,你还需要告诉table view这两个标签的高度。
选择table view,然后打开尺寸检查器,设置Row Height为57。
我们现在来为这个视图控制器来写代码。你应该已经对table view controllers轻车熟路了。
添加一个新的swift文件到工程中,命名为LocationsViewController.swift。
将其中的代码替换为:
import UIKit
import CoreData
import CoreLocation
class LocationsViewController: UITableViewController {
var managedObjectContext: NSManagedObjectContext!
// MARK: - UITableViewDataSource
override func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
return 1 }
override func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "LocationCell", for: indexPath)
let descriptionLabel = cell.viewWithTag(100) as! UILabel
descriptionLabel.text = "If you can see this"
let addressLabel = cell.viewWithTag(101) as! UILabel
addressLabel.text = "Then it works!"
return cell
}
}
你在标签中伪造一行占位符文本。 你也已经给这个类NSManagedObjectContext属性,虽然你现在不会使用它。
运行app,确保一切工作正常。
注意:如果列表仍然是空的,那么就回到storyboard,打开这个视图控制器的身份检查器,Module(模块)字段此时应该是显示None,点击旁边的蓝色箭头,在下拉选项中选择Mylocations。
Swift app由一个或多个模块组成。你工程中的每个目标都被编译进带有自己域名空间的模块中去,并且界面建造器需要知道你的视图控制器的类存在于哪个模块中。
由于你是在创建LocationsViewController之前就填写了Class字段,所以Xcode会感到无所适从,使用command+S保存一下,然后再次运行app试试。
非常不错,这次列表被数据存储中的Location对象填满了。
运行app并标记几个位置。 如果数据存储中没有数据,
那么app则不会展示什么东西。。。
app的这个新部分还不知道有关你添加到数据存储的Location对象的任何信息。 为了在列表中显示它们,你需要以某种方式获得对这些对象的引用。 你可以通过询问数据存储来做到这一点。 这被称为抓取(fetching)。
首先,在LocationsViewController.swift中添加一个新的实例变量:
var locations = [Location]()
这个数组包含一个Location对象的列表。
添加viewDidLoad()方法:
override func viewDidLoad() {
super.viewDidLoad()
// 1
let fetchRequest = NSFetchRequest<Location>()
// 2
let entity = Location.entity()
fetchRequest.entity = entity
// 3
let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do { // 4
locations = try managedObjectContext.fetch(fetchRequest)
} catch {
fatalCoreDataError(error)
}
}
看起来好像很复杂,但是实际上很简单。你将要求托管对象上下文查找数据存储中所有Location对象的列表,并按日期排序。
1、NSFetchRequest的对象描述了你要从数据存储中获取哪些对象。 要检索先前保存到数据存储区的对象,你需要创建一个描述该对象(或多个对象)的搜索参数的获取请求。
2、这里你告诉抓取请求,你正在寻找Location的实体。
3、NSSortDescriptor通知抓取请求按日期属性对其进行升序排序。 换句话说,用户首先添加的Location对象将位于列表的顶部。 你可以在此处对任何属性进行排序(稍后在本教程中,你还将对位置的类别进行排序)。
4、现在你有了抓取请求,你可以告诉上下文执行它。fetch()方法返回一个包含排序对象的数组,或者在出错时抛出一个错误。 这就是do-try-catch的作用。
如果一切顺利,你则将抓取结果分配给Locations实例变量。
⚠️:创建抓取请求的写法是NSFetchRequest <Location>。
这里的<>意味着NSFetchRequest是一个泛型。 回想一下,数组也是泛型,因为要创建数组,可以使用简写符号[Location]或更长的Array <Location>来指定进入数组的对象的类型。
要使用NSFetchRequest,你需要告诉它你要获取什么类型的对象。 在这里,你创建一个NSFetchRequest <Location>,以便fetch()的结果是一个Location对象的数组。
现在你已将Location对象的列表加载到实例变量中,你可以更改表视图的数据源方法。
将要数据源方法修改为:
override func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
return return locations.count
}
override func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "LocationCell", for: indexPath)
let location = locations[indexPath.row]
let descriptionLabel = cell.viewWithTag(100) as! UILabel
descriptionLabel.text = location.locationDescription
let addressLabel = cell.viewWithTag(101) as! UILabel
if let placemark = location.placemark {
var text = ""
if let s = placemark.subThoroughfare {
text += s + " " }
if let s = placemark.thoroughfare {
text += s + ", "
}
if let s = placemark.locality {
text += s }
addressLabel.text = text
} else {
addressLabel.text = ""
}
return cell
}
这些东西你都应该很熟悉了。你从数组中得到某一行的Location对象,之后使用它的属性填满列表。因为placemark是一个可选型,你需要使用if let来解包。
运行app,然后切换到Locations界面,果然,app挂了。
在调试区域应该打印了如下信息:
fatal error: unexpectedly found nil while unwrapping an Optional value
练习:你知道自己遗漏了什么吗?
答案:你在LocationsViewController中添加了一个managedObjectContext属性,但是从来没有给它一个值。因此,Location对象中取不到任何东西。
切换到AppDelegate.swift文件。在application(didFinishLaunchingWithOptions)方法中的if let tabBarViewControllers语句块中添加以下语句:
let navigationController = tabBarViewControllers[1]
as! UINavigationController
let locationsViewController = navigationController.viewControllers[0]
as! LocationsViewController
locationsViewController.managedObjectContext = managedObjectContext
这样就会从storyboard中找到LocationsViewController,并且给它一个关于管理对象上下文(managed object context)的引用。
再次运行app,切换到Locations界面。Core Data可以顺利的获取Location对象,并且展示它们了。
请注意,如果你标记新位置,此时列表不会自动更新。 你必须重新启动
该应用程序将显示新的Location对象。 你会在本教程的后面解决这个问题。
Creating a custom Table View Cell subclass—创建一个自定义的Table View Cell子类
使用cell.viewWithTag(xxx)来获取table view cell中的label,确实可以达到目的,但是对我而言这种方法非常不“面向对象”。
如果你能够创建自己的UITableViewCell子类并且给它一个用于label的outlet,那就完美了。幸运的是,你可以很容易的实现这个目的。
使用Cocoa Touch Class模版添加一个新的文件到工程中去。命名为LocationCell,并且指定其为UITableViewCell的子类。
在新增的LocationCell.swift文件中添加如下outlet,注意,写在class的内部。
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var addressLabel: UILabel!
打开storyboard并且选择你之前创建的cell。然后打开它的身份检查器,将Class字段设置为Locationcell。
现在你可以将两个标签和刚才的两个outlet连接起来了。这次outlet并不在视图控制器上,而是在cell上,所以我们需要使用LocationCell的连接指示器来完成这一工作。
这就是让table view使用你自己创建的table view cell class所需的全部工作。你还需要修改一下LocationsViewController。
打开LocationsViewController.swift,将tableView(cellForRowAt) 方法替换为下面的版本:
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "LocationCell", for: indexPath) as! LocationCell
let location = locations[indexPath.row]
cell.configure(for: location)
return cell
}
和以前一样,这里向cell请求使用 dequeueReusableCell(withIdentifier, for)方法,但是目前LocationCell对象取代了标准的UITableViewCell。所以上面的代码中你用到了角色扮演。
请注意,“LocationCell”是占位符cell中的重用标识符,但LocationCell是你正在获取的实际cell对象的类。 它们具有相同的名称,但其中一个是String,另一个是具有额外属性的UITableViewCell子类。 我希望这没有让你弄混。
在获取了cell的引用后,你跳用了一个新的方法,叫做configure(for) ,现在我们来添加这个方法。
打开LocationCell.swift,添加以下代码:
func configure(for location: Location) {
if location.locationDescription.isEmpty {
descriptionLabel.text = "(No Description)"
} else {
descriptionLabel.text = location.locationDescription
}
if let placemark = location.placemark {
var text = ""
if let s = placemark.subThoroughfare {
text += s + " " }
if let s = placemark.thoroughfare {
text += s + ", "
}
if let s = placemark.locality {
text += s }
addressLabel.text = text
} else {
addressLabel.text = String(format:
"Lat: %.8f, Long: %.8f", location.latitude, location.longitude)
}
}
你现在只需使用cell的descriptionLabel和addressLabel属性,而不是使用viewWithTag()来查找描述和地址标签。
运行app确保一切仍然有效。 如果没有placemark,address等信息,table view cell现在会显示“(No Description)”。
你可以创建任意你想要的table view cell子类。
注意,如果Xcode一直报错,提示:“LocationCell has no member configure”,使用command+B多编译几次,或者退出Xcode再重新打开。