Swift From Scratch:初始化和初始化委托

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类的实例进行编码和解码时,才有可能。

不过请放心,这不是火箭科学。 我们只需要使TaskToDo类符合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类的实例进行编码。

requiredinit?(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:) 。 这被称为委派
  • 即使便捷初始化程序不必将初始化委托给指定的初始化程序,便捷初始化程序也需要在某个时候调用指定的初始化程序 。 这是完全初始化正在初始化的实例所必需的。

有了两个模型类,现在该重构ViewControllerAddItemViewController类了。 让我们从后者开始。

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中阅读有关初始化和初始化程序委托的更多信息。

翻译自: https://code.tutsplus.com/tutorials/swift-from-scratch-initialization-and-initializer-delegation--cms-23538

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值