在Swift From Scratch的上一课中,我们创建了一个功能性的待办应用程序。 不过,数据模型可能需要一些帮助。 在最后的课程中,我们将通过实现自定义模型类来重构数据模型。
1.数据模型
我们将要实现的数据模型包括两个类,一个Task
类和一个从Task
类继承的ToDo
类。 在创建和实现这些模型类的同时,我们将继续探索Swift中的面向对象编程。 在本课程中,我们将放大类实例的初始化以及初始化期间继承扮演的角色。
Task
班
让我们从Task
类的实现开始。 通过从Xcode的File菜单中选择New> File ...创建一个新的Swift文件。 从“ iOS”>“源”部分中选择“ Swift文件 ”。 将文件命名为Task.swift并点击Create 。
基本实现是简短的。 Task
类继承自Foundation框架中定义的NSObject
,并具有类型为String
的可变属性name
。 该类定义了两个初始化器, init()
和init(name:)
。 有一些细节可能会让您绊倒,所以让我解释一下发生了什么。
import Foundation
class Task: NSObject {
var name: String
convenience override init() {
self.init(name: "New Task")
}
init(name: String) {
self.name = name
}
}
因为init()
方法也是在NSObject
类中定义的,所以我们需要在初始化器的前面加上override
关键字。 我们在本系列的前面部分介绍了覆盖方法。 在init()
方法中,我们调用init(name:)
方法,并传入"New Task"
作为name
参数的值。
init(name:)
方法是另一个初始化程序,它接受String
类型的单个参数name
。 在此初始化程序中,将name
参数的值分配给name
属性。 这很容易理解。 对?
指定和便捷初始化器
带有init()
方法前缀的convenience
关键字是什么? 类可以具有两种类型的初始化程序,即指定的初始化程序和便捷的初始化程序。 便捷初始化程序的前缀为convenience
关键字,这表示init(name:)
是指定的初始化程序。 这是为什么? 指定的初始化和便捷的初始化之间有什么区别?
指定的初始化程序将完全初始化类的实例,这意味着实例的每个属性在初始化后均具有初始值。 例如,查看Task
类,我们看到name
属性是使用init(name:)
初始化程序的name
参数的值设置的。 初始化后的结果是一个完全初始化的Task
实例。
但是, 便利的初始化程序依赖于指定的初始化程序来创建类的完全初始化的实例。 这就是Task
类的init()
初始化程序在其实现中调用init(name:)
初始化程序的原因。 这称为初始化程序委托 。 init()
初始化程序将初始化委托给指定的初始化程序,以创建Task
类的完全初始化的实例。
便捷初始化器是可选的。 并非每个类都有一个便利的初始化程序。 需要指定的初始化程序,并且一个类需要至少具有一个指定的初始化程序才能创建其自身的完全初始化的实例。
NSCoding
协议
但是, Task
类的实现尚未完成。 在本课程的后面,我们将一个ToDo
实例数组写入磁盘。 仅当可以对ToDo
类的实例进行编码和解码时,才有可能。
不过请放心,这不是火箭科学。 我们只需要使Task
和ToDo
类符合NSCoding
协议即可。 这就是为什么Task
从类继承NSObject
,因为该类NSCoding
协议只能通过类继承,直接或间接地从实施NSObject
。 像NSObject
类一样, NSCoding
协议在Foundation框架中定义。
在本系列中我们已经介绍了采用协议,但是我要指出一些陷阱。 让我们从告诉编译器Task
类符合NSCoding
协议开始。
import Foundation
class Task: NSObject, NSCoding {
var name: String
...
}
接下来,我们需要实现NSCoding
协议中声明的两个方法init?(coder:)
和encode(with:)
。 如果您熟悉NSCoding
协议,则实现非常简单。
import Foundation
class Task: NSObject, NSCoding {
var name: String
@objc required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String
}
@objc func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
}
convenience override init() {
self.init(name: "New Task")
}
init(name: String) {
self.name = name
}
}
init?(coder:)
初始化程序是用于初始化Task
实例的指定初始化程序。 即使我们实现了符合NSCoding
协议的init?(coder:)
方法,您也无需直接调用此方法。 对于encode(with:)
,也是如此,它对Task
类的实例进行编码。
required
以init?(coder:)
方法为前缀的关键字表示Task
类的每个子类都需要实现此方法。 required
关键字仅适用于初始化程序,这就是为什么我们不需要将其添加到encode(with:)
方法中的原因。
在继续之前,我们需要讨论@objc
属性。 由于NSCoding
协议是Objective-C协议,因此只能通过添加@objc
属性来检查协议一致性 。 在Swift中,没有协议一致性或可选协议方法之类的东西。 换句话说,如果一个类遵循特定的协议,则编译器将验证并期望实现该协议的每种方法。
ToDo
类
实现了Task
类之后,就该实现ToDo
类了。 创建一个新的Swift文件并将其命名为ToDo.swift 。 让我们看一下ToDo
类的实现。
import Foundation
class ToDo: Task {
var done: Bool
@objc required init?(coder aDecoder: NSCoder) {
self.done = aDecoder.decodeBool(forKey: "done")
super.init(coder: aDecoder)
}
@objc override func encode(with aCoder: NSCoder) {
aCoder.encode(done, forKey: "done")
super.encode(with: aCoder)
}
init(name: String, done: Bool) {
self.done = done
super.init(name: name)
}
}
该ToDo
从类继承Task
类,并声明一个变量属性done
类型的Bool
。 除了从Task
类继承的NSCoding
协议的两个必需方法外,它还声明了一个指定的初始化程序init(name:done:)
。
像在Objective-C中一样, super
关键字引用超类,在此示例中为Task
类。 有一个重要的细节值得关注。 在超类上调用init(name:)
方法之前,必须初始化ToDo
类声明的每个属性。 换句话说,在之前ToDo
类代表初始化它的父类,由定义的每个属性ToDo
类需要有一个有效的初始值。 您可以通过切换语句的顺序并检查弹出的错误来验证这一点。
这同样适用于init?(coder:)
方法。 我们首先在父类上调用init?(coder:)
之前初始化done
属性。
初始化程序和继承
在处理继承和初始化时,需要牢记一些规则。 指定初始化程序的规则很简单。
- 指定的初始化程序需要从其超类调用指定的初始化程序。 例如,在
ToDo
类中,init?(coder:)
方法调用其超类的init?(coder:)
方法。 这也称为委派 。
便利初始化程序的规则稍微复杂一些。 要记住两个规则。
- 便捷初始化程序始终需要调用其定义的类的另一个初始化程序。例如,在
Task
类中,init()
方法是便捷初始化程序,并将初始化委托给另一个初始化程序,在示例中为init(name:)
。 这被称为委派 。 - 即使便捷初始化程序不必将初始化委托给指定的初始化程序,便捷初始化程序也需要在某个时候调用指定的初始化程序 。 这是完全初始化正在初始化的实例所必需的。
有了两个模型类,现在该重构ViewController
和AddItemViewController
类了。 让我们从后者开始。
2.重构AddItemViewController
步骤1:更新AddItemViewControllerDelegate
协议
我们需要在AddItemViewController
类中进行的唯一更改与AddItemViewControllerDelegate
协议有关。 在协议声明中,将didAddItem
的类型从String
更改为ToDo
,这是我们之前实现的模型类。
protocol AddItemViewControllerDelegate {
func controller(_ controller: AddItemViewController, didAddItem: ToDo)
}
步骤2:更新create(_:)
操作
这意味着我们还需要更新其中调用委托方法的create(_:)
动作。 在更新的实现中,我们创建一个ToDo
实例,并将其传递给委托方法。
@IBAction func create(_ sender: Any) {
if let name = textField.text {
// Create Item
let item = ToDo(name: name, done: false)
// Notify Delegate
delegate?.controller(self, didAddItem: item)
}
}
3.重构ViewController
步骤1:更新items
属性
ViewController
类需要更多的工作。 我们首先需要将items
属性的类型更改为[ToDo]
,这是ToDo
实例的数组。
var items: [ToDo] = [] {
didSet(oldValue) {
let hasItems = items.count > 0
tableView.isHidden = !hasItems
messageLabel.isHidden = hasItems
}
}
步骤2:表格视图数据源方法
这也意味着我们需要重构其他一些方法,例如下面所示的tableView(_:cellForRowAt:)
方法。 因为items
数组现在包含ToDo
实例,所以检查项目是否标记为完成要简单得多。 我们使用Swift的三元条件运算符来更新表格视图单元格的附件类型。
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.name
cell.accessoryType = item.done ? .checkmark : .none
return cell
}
当用户删除项目时,我们只需要通过删除相应的ToDo
实例来更新items
属性。 这反映在下面显示的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)
// Save State
saveItems()
}
}
步骤3:表格视图委托方法
在用户点击一行时更新项目的状态由tableView(_:didSelectRowAt:)
方法处理。 ToDo
类,此UITableViewDelegate
方法的实现要简单得多。
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Fetch Item
let item = items[indexPath.row]
// Update Item
item.done = !item.done
// Fetch Cell
let cell = tableView.cellForRow(at: indexPath)
// Update Cell
cell?.accessoryType = item.done ? .checkmark : .none
// Save State
saveItems()
}
相应的ToDo
实例已更新,并且此更改反映在表视图中。 为了保存状态,我们调用saveItems()
而不是saveCheckedItems()
。
步骤4:添加Item View Controller委托方法
因为我们更新了AddItemViewControllerDelegate
协议,所以我们还需要更新该协议的ViewController
实现。 但是,更改很简单。 我们只需要更新方法签名。
func controller(_ controller: AddItemViewController, didAddItem: ToDo) {
// Update Data Source
items.append(didAddItem)
// Save State
saveItems()
// Reload Table View
tableView.reloadData()
// Dismiss Add Item View Controller
dismiss(animated: true)
}
步骤5:保存项目
pathForItems()
方法
与其将项目存储在用户默认数据库中,不如将它们存储在应用程序的documents目录中。 在更新loadItems()
和saveItems()
方法之前,我们将实现一个名为pathForItems()
的辅助方法。 该方法是私有的,并返回路径,项目在documents目录中的位置。
private func pathForItems() -> String {
guard let documentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first,
let url = URL(string: documentsDirectory) else {
fatalError("Documents Directory Not Found")
}
return url.appendingPathComponent("items").path
}
我们首先通过调用NSSearchPathForDirectoriesInDomains(_:_:_:)
来获取应用程序沙箱中documents目录的路径。 由于此方法返回字符串数组,因此我们获取了第一项。
请注意,我们使用guard
语句来确保NSSearchPathForDirectoriesInDomains(_:_:_:)
返回的值有效。 如果此操作失败,则会引发致命错误。 这将立即终止应用程序。 我们为什么要做这个? 如果操作系统无法将文件路径传递给我们,则我们有更大的问题要担心。
我们从pathForItems()
返回的值由文档目录的路径组成,并附加了字符串"items"
。
loadItems()
方法
loadItems方法的变化很大。 我们首先将pathForItems()
的结果存储在一个常量path
。 然后,我们取消归档在该路径中归档的对象,并将其向下转换为可选的ToDo
实例数组。 我们使用可选绑定解开可选对象,并将其分配给常量items
。 在if
子句中,我们将存储在items
的值分配给items
属性。
private func loadItems() {
let path = pathForItems()
if let items = NSKeyedUnarchiver.unarchiveObject(withFile: path) as? [ToDo] {
self.items = items
}
}
saveItems()
方法
saveItems()
方法简短而简单。 我们将pathForItems()
的结果存储在常量path
,并在NSKeyedArchiver
上调用archiveRootObject(_:toFile:)
,并传入items
属性和path
。 我们将操作结果打印到控制台。
private func saveItems() {
let path = pathForItems()
if NSKeyedArchiver.archiveRootObject(self.items, toFile: path) {
print("Successfully Saved")
} else {
print("Saving Failed")
}
}
步骤6:清理
让我们以有趣的部分结尾,删除代码。 首先删除顶部的checkedItems
属性,因为我们不再需要它。 结果,我们还可以删除loadCheckedItems()
和saveCheckedItems()
方法,以及ViewController
类中对这些方法的所有引用。
生成并运行该应用程序,以查看是否一切仍然正常。 数据模型使应用程序的代码更简单,更可靠。 多亏了ToDo
类,现在可以更轻松地管理列表中的项目,并且不易出错。
结论
在本课程中,我们重构了应用程序的数据模型。 您了解了有关面向对象的编程和继承的更多信息。 实例初始化是Swift中一个重要的概念,因此请确保您了解本课中介绍的内容。 您可以在The Swift Programming Language中阅读有关初始化和初始化程序委托的更多信息。