在上一课中 ,我们添加了创建待办事项的功能。 尽管此添加使应用程序更有用,但添加将项目标记为已完成并删除项目的功能也将很方便。 这就是本课中要重点讨论的内容。
先决条件
如果您想跟我一起学习,请确保您的计算机上安装了Xcode 8.3.2或更高版本。 您可以从Apple的App Store下载Xcode 8.3.2。
1.删除项目
要删除项目,我们需要实现UITableViewDataSource
协议的两个附加方法。 我们首先需要通过实现tableView(_:canEditRowAt:)
方法来告诉表视图可以编辑哪些行。 正如您在下面的代码片段中看到的那样,实现非常简单。 我们通过返回true
告诉表视图每行都是可编辑的。
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
我们感兴趣的第二种方法是tableView(_:commit:forRowAt:)
。 实现有点复杂,但足够容易掌握。
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Update Items
items.remove(at: indexPath.row)
// Update Table View
tableView.deleteRows(at: [indexPath], with: .right)
}
}
我们首先检查editingStyle
的值,该值是UITableViewCellEditingStyle
类型的枚举。 仅当editingStyle
的值等于UITableViewCellEditingStyle.delete
才删除项目。
不过,Swift比这更聪明。 因为它知道editingStyle
是UITableViewCellEditingStyle
类型,所以我们可以省略UITableViewCellEditingStyle
(枚举的名称),并编写.delete
(我们感兴趣的枚举的成员值)。如果您是Swift枚举的新手,那么我建议您阅读有关Swift中枚举的快速提示 。
接下来,我们通过在items
属性上调用remove(at:)
并传递正确的索引来更新表视图的数据源items
。 我们还通过在tableView
上调用deleteRows(at:with:)
来更新表视图,并传入带有indexPath
和.right
的数组以指定动画类型。 如前所述,我们可以省略枚举的名称UITableViewRowAnimation
,因为Swift知道第二个参数的类型是UITableViewRowAnimation
。
用户现在应该能够从列表中删除项目。 生成并运行应用程序以对此进行测试。
2.检查项目
要将项目标记为完成,我们将在对应的行上添加一个选中标记。 这意味着我们需要跟踪用户已标记为完成的项目。 为此,我们将声明一个新属性来为我们管理此属性。 声明类型为[String]
的变量属性checkedItems
,并使用一个空数组对其进行初始化。
var checkedItems: [String] = []
在tableView(_:cellForRowAt:)
,我们通过调用contains(_:)
方法并传入与当前行相对应的项目,来检查checkedItems
是否包含相应的项目。 如果checkedItems
包含item
则该方法返回true
。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch Item
let item = items[indexPath.row]
// Dequeue Cell
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath)
// Configure Cell
cell.textLabel?.text = item
if checkedItems.contains(item) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
如果item
被发现checkedItems
,我们设置了电池的accessoryType
属性.checkmark
的成员值UITableViewCellAccessoryType
枚举。 如果未找到item
,我们将退回到.none
作为单元格的附件类型。
下一步是通过实现UITableViewDelegate
协议的tableView(_:didSelectRowAt:)
方法来添加将项目标记为完成的功能。 在此委托方法中,我们首先在tableView
上调用deselectRow(at:animated:)
以取消选择用户点击的行。
// MARK: - Table View Delegate Methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Fetch Item
let item = items[indexPath.row]
// Fetch Cell
let cell = tableView.cellForRow(at: indexPath)
// Find Index of Item
let index = checkedItems.index(of: item)
if let index = index {
checkedItems.remove(at: index)
cell?.accessoryType = .none
} else {
checkedItems.append(item)
cell?.accessoryType = .checkmark
}
}
然后,我们从items
获取对应的项目,并获取与所点击的行相对应的单元格的引用。 我们通过调用index(of:)
向checkedItems
询问相应项的index(of:)
。 此方法返回一个可选的Int
。 如果checkedItems
包含item
,则将其从checkedItems
删除,并将单元格的附件类型设置为.none
。 如果checkedItems
不包含item
,则将其添加到checkedItems
,并将单元格的附件类型设置为.checkmark
。
通过这些添加,用户现在可以将项目标记为已完成。 生成并运行该应用程序,以确保一切正常。
3.保存状态
该应用程序当前不保存两次启动之间的状态。 为了解决这个问题,我们将把items
和checkedItems
数组存储在应用程序的用户默认数据库中。
步骤1:载入状态
首先创建两个帮助器方法loadItems()
和loadCheckedItems()
。 请注意,在每个帮助程序方法之前加了private
关键字。 private
关键字告诉Swift,这些方法只能从ViewController
类中访问。
// MARK: Private Helper Methods
private func loadItems() {
let userDefaults = UserDefaults.standard
if let items = userDefaults.object(forKey: "items") as? [String] {
self.items = items
}
}
private func loadCheckedItems() {
let userDefaults = UserDefaults.standard
if let checkedItems = userDefaults.object(forKey: "checkedItems") as? [String] {
self.checkedItems = checkedItems
}
}
private
关键字是Swift的访问控制的一部分 。 顾名思义,访问控制定义了哪些代码可以访问哪些代码。 访问级别适用于方法,函数,类型等。Apple仅指实体 。 有五个访问级别:开放,公共,内部,文件专用和专用。
- 开放/公开 :标记为开放或公开的实体可由同一模块以及其他模块中定义的实体访问。 这是公开框架接口的理想选择。 开放访问级别和公共访问级别之间存在一些差异。 您可以在The Swift Programming Language中阅读有关这些差异的更多信息。
- 内部:这是默认访问级别。 换句话说,如果未指定访问级别,则应用此访问级别。 内部访问级别的实体只能由同一模块中定义的实体访问。
- 文件专用:声明为文件专用的实体只能由同一源文件中定义的实体访问。 例如,在
ViewController
类中定义的私有帮助器方法只能由ViewController
类访问。
- 私有:私有与文件私有非常相似。 唯一的区别是,声明为私有的实体只能从其包围的声明中进行访问。 例如,如果我们在ViewController.swift中为
ViewController
类创建扩展,则在扩展中将无法访问任何标记为file-private的实体,但可以访问私有实体。
如果您熟悉UserDefaults
类,则辅助方法的实现很简单。 为了易于使用,我们将对标准用户默认对象的引用存储在名为userDefaults
的常量中。 对于loadItems()
,我们向userDefaults
请求与键"items"
关联的对象,并将其向下转换为可选的字符串数组。 我们安全地解开了可选项,这意味着如果可选项不是nil
,则将值存储在常量items
,并将该值分配给视图控制器的items
属性。
如果if
语句看起来令人困惑,则在下面的示例中查看一下loadItems()
方法的简单版本。 结果是相同的; 唯一的区别是简洁。
private func loadItems() {
let userDefaults = UserDefaults.standard
let storedItems = userDefaults.object(forKey: "items") as? [String]
if let items = storedItems {
self.items = items
}
}
除了用于加载存储在用户默认数据库中的对象的键之外, loadCheckedItems()
的实现是相同的。 让我们通过更新viewDidLoad()
方法来使用loadItems()
和loadCheckedItems()
。
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "To Do"
// Populate Items
items = ["Buy Milk", "Finish Tutorial", "Play Minecraft"]
// Load State
loadItems()
loadCheckedItems()
// Register Class for Cell Reuse
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "TableViewCell")
}
步骤2:保存状态
为了保存状态,我们实现了另外两个私有帮助器方法saveItems()
和saveCheckedItems()
。 逻辑类似于loadItems()
和loadCheckedItems()
。 区别在于我们将数据存储在用户默认数据库中。 确保setObject(_:forKey:)
调用中使用的键与loadItems()
和loadCheckedItems()
使用的键匹配。
private func saveItems() {
let userDefaults = UserDefaults.standard
// Update User Defaults
userDefaults.set(items, forKey: "items")
userDefaults.synchronize()
}
private func saveCheckedItems() {
let userDefaults = UserDefaults.standard
// Update User Defaults
userDefaults.set(checkedItems, forKey: "checkedItems")
userDefaults.synchronize()
}
并非必须要调用synchronize()
。 操作系统将确保您在用户的默认数据库存储数据在某个时刻写入磁盘。 但是,通过调用synchronize()
,您可以明确地告诉操作系统将所有挂起的更改写入磁盘。 这在开发过程中很有用,因为如果您杀死应用程序,操作系统不会将您的更改写入磁盘。 然后,似乎有些事情无法正常工作。
我们需要在许多地方调用saveItems()
和saveCheckedItems()
。 首先,在将新项目添加到列表时调用saveItems()
。 我们在AddItemViewControllerDelegate
协议的委托方法中执行此操作。
// MARK: Add Item View Controller Delegate Methods
func controller(_ controller: AddItemViewController, didAddItem: String) {
// Update Data Source
items.append(didAddItem)
// Save State
saveItems()
// Reload Table View
tableView.reloadData()
// Dismiss Add Item View Controller
dismiss(animated: true)
}
当项目的状态在tableView(_:didSelectRowAt:)
更改时,我们将更新checkedItems
。 此时最好也调用saveCheckedItems()
。
// MARK: - Table View Delegate Methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Fetch Item
let item = items[indexPath.row]
// Fetch Cell
let cell = tableView.cellForRow(at: indexPath)
// Find Index of Item
let index = checkedItems.index(of: item)
if let index = index {
checkedItems.remove(at: index)
cell?.accessoryType = .none
} else {
checkedItems.append(item)
cell?.accessoryType = .checkmark
}
// Save State
saveCheckedItems()
}
删除items
, items
和checkedItems
更新。 为了保存此更改,我们同时调用saveItems()
和saveCheckedItems()
。
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Fetch Item
let item = items[indexPath.row]
// Update Items
items.remove(at: indexPath.row)
if let index = checkedItems.index(of: item) {
checkedItems.remove(at: index)
}
// Update Table View
tableView.deleteRows(at: [indexPath], with: .right)
// Save State
saveItems()
saveCheckedItems()
}
}
而已。 生成并运行该应用程序以测试您的工作。 玩应用程序并强制退出它。 当您再次启动该应用程序时,最后一个已知状态应被加载并可见。
4.物业观察员
目前尚缺乏该应用程序的用户体验。 删除所有项目或首次启动应用程序时,用户将看到一个空的表视图。 不好 我们可以通过在没有物品时显示一条消息来解决此问题。 这也使我有机会向您展示Swift的另一个功能,即属性观察器 。
步骤1:添加标签
首先,向用户界面添加标签以显示消息。 在ViewController
类中声明一个名为UILabel
名为messageLabel
的插座,打开Main.storyboard ,然后在视图控制器的视图中添加一个标签。
@IBOutlet var messageLabel: UILabel!
将必需的布局约束添加到标签,并将其与Connections Inspector中的视图控制器的messageLabel
插座连接 。 将标签的文本设置为“ 您没有待办事项”。 并将标签的文本放在Attributes Inspector中 。
![添加消息标签](https://i-blog.csdnimg.cn/blog_migrate/4678dfaca59190cf18a136be719e2aa6.png)
步骤2:实现属性观察器
仅当items
包含任何元素时,消息标签才应该可见。 发生这种情况时,我们还应该隐藏表格视图。 我们可以通过在ViewController
类中添加各种检查来解决此问题,但是更方便,更优雅的方法是使用属性观察器。
顾名思义,属性观察者观察属性。 每当属性更改时,即使新值与旧值相同,也会调用属性观察器。 有两种类型的属性观察器。
-
willSet
:在值更改之前调用 -
didSet
:值更改后调用
为了我们的目的,我们将为items
属性实现didSet
观察器。 看一下下面的代码片段中的语法。
var items: [String] = [] {
didSet {
let hasItems = items.count > 0
tableView.isHidden = !hasItems
messageLabel.isHidden = hasItems
}
}
起初该结构可能看起来有些奇怪,所以让我解释一下发生了什么。 调用didSet
属性观察器时,更改items
属性后,我们将检查items
属性是否包含任何元素。 基于hasItems
常量的值,我们更新用户界面。 就这么简单。
didSet
观察器被传递了一个常数参数,该常数包含该属性的旧值的值。 上面的示例中省略了它,因为在实现中我们不需要它。 以下示例显示了如何使用它。
var items: [String] = [] {
didSet(oldValue) {
if oldValue != items {
let hasItems = items.count > 0
tableView.isHidden = !hasItems
messageLabel.isHidden = hasItems
}
}
}
示例中的oldValue
参数没有显式类型,因为Swift知道items
属性的类型。 在该示例中,仅当旧值与新值不同时,我们才更新用户界面。
willSet
观察者的工作方式与此类似。 主要区别在于传递给willSet
观察器的参数是一个常数,用于保存属性的新值。 使用属性观察器时,请记住,实例初始化时不会调用它们。
生成并运行该应用程序,以确保所有内容均正确连接。 即使该应用程序不是完美的并且可以使用更多功能,您还是使用Swift创建了第一个iOS应用程序。
结论
在本系列的最后三课中,您使用Swift的面向对象功能创建了功能性iOS应用程序。 如果您有一些编程和开发应用程序的经验,那么您肯定已经注意到,当前的数据模型存在一些不足之处,因此请轻描淡写。
如果要构建适当的应用程序,将项目存储为字符串并创建一个单独的数组来存储项目的状态并不是一个好主意。 更好的方法是创建一个单独的ToDo
类以对项目进行建模并将其存储在应用程序的沙箱中。 这将是本系列下一课的目标。