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再重新打开。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容