Tryswift daniel steinberg cover cn

文化碰撞:函数式、面向协议、面向对象编程的最佳实践

编写一个 Swift 应用不仅仅是将某个 Objective-C 应用翻译为 Swift,我们还需要采纳 Swift 语言的特点和思想。在这次 try! Swift 的演讲当中,我们从一个翻译为 Swift 的标准 MVC 表视图应用开始,逐步应用函数式编程、面向对象编程、设计模式以及面向协议编程,从而让这个应用符合 Swift 的语言习惯。


Swift 启航 (0:00)

通常情况下,大家往往是通过将一个既有的 Objective-C 代码转换为 Swift 语法来开始编写 Swift 代码的。接着,我们就可以前往一个新的境界,引入我们以前就知道的那些特性:面向对象编程、函数式编程,以及面向协议编程,然后将它们揉和在一起。

我会以一个 Table View 的示例开始。如果您打开 Xcode 的话,您可能会直接使用这个模板(这样子很丢人的),然后您开始编写控制器、模型和视图的代码。首先,我只是将这段代码翻译成 Swift 而已,因此我创建了视图,当然也可以使用 Storyboard。对于模型来说,我准备从一副牌中发五张牌,然后称之为 “一手牌(Hand)”。

“Hand” 拥有大小和花色,因此我用某种方式对其进行表示,此外,我还必须要通过控制器来管理模型和视图。我会使用一个 HandViewController 来进行管理,它是 UITableViewController 的子类。我们会以一个常规的方式开始,这往往也是我们都会做的,这意味着视图控制器知道很多关于模型的信息。(这并不是一件好事)。

让我们来看一下具体代码。

Basic view controller (1:56)

让我们从基础的视图控制器开始。我的控制器类全称是 HandViewController,不过为了简单起见,我给它取名为 HandVC,它是 UITableViewController 的子类。

// HandVC.swift
import UIKit

class HandVC: UITableViewController {
    private let hand = Hand()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.leftBarButtonItem = self.editButtionItem()
    }
    
    @IBAction private func addNewCard(sender: UIBarButtionItem) {
        if hand.numberOfCards < 5 {
            hand.addNewCardAtIndex(0)
            tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Fade)
        }
    }

对于 Swift 来说,我最喜欢的事情之一就是我们创建属性的方式了。我们不用将声明和初始化的操作进行分离。我们只需要说『这是我创建的属性』,这样视图控制器就知晓了这个模型的种种信息。接下来,当我按下 “plus” 按钮的时候,这个动作方法 addNewCard(_:) 就会被调用。

如果我们的牌数少于 5 张的话,那么我们就还可以添加新的牌。一如既往,我们添加另一张牌,将新的牌添加到模型当中,然后再在视图中展示出来。我倾向于将这段代码单独提取出来,这样的话我的动作方法就非常简单。

@IBAction private func addNewCard(sender: UIBarButtionItem) {
    if hand.numberOfCards < 5 {
        hand.addNewCardAtIndex(0)
        insertTopRow()
    }
}

private func insertTopRow() {
        tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Fade)
}

这样,我首先对模型进行了处理,随后对视图进行了处理。我将视图处理的方法提取了出来,这样您就会看到,这允许我们可以在别的地方重用这段代码。我尽可能地让我的方法保持精简。

// MARK: - Table View Data Source

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return hand.numberOfCards
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cardCell", forIndexPath: indexPath)
    let card = hand.cardAtPosition(indexPath.row)
    cell.textLabel?.text = card.rank.description
    cell.textLabel?.textColor = card.color
    cell.detailTextLabel?.text = card.suit.description
    return cell
}

Receive news and updates from Realm straight to your inbox

这里实现了两个表视图数据源方法。第一个指出了每个 Section 当中有多少行数据,我们通过模型进行询问:『模型你好,你里面有多少张牌啊?』。

另一个方法是用于填充表视图单元格的。对于表视图的每行单元格来说,我们获取对应的模型对象,然后设置牌的大小和颜色,然后将花色展示出来。现在,如果我们还需要删除牌的话,那么我们可以实现这个方法,和我们执行的添加动作类似,我们从模型中将牌删除掉。接着再将这个对应的视图移除。

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        hand.deleteCardAtIndex(indexPath.row)
        deleteRowAtIndexPath(indexPath)
    }
}

private func deleteRowAtIndexPath(indexPath: NSIndexPath) {
    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}

我们将这个视图移除,然后我们在另一个单独的方法中放入这个视图移除代码。注意到,这是私有方法,因为不应该有别的视图能够使用这个功能。

override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
    hand.moveCard(fromIndexPath.row, toIndex: toIndexPath.row)
}

要移动单元格的话,我们不需要执行任何的 GUI 代码。Apple 已经为我们提供了这个功能了。我们所需要做的,就是执行我们的模型代码,告诉它『我要将这张牌从这里移动到那里』。这就是我们的视图控制器的全部了。让我们来看一看模型是如何实现的。

基础模型 (4:45)

import UIKit

class Hand {
    private let deck = Deck()
    private var cards = [Card]()

这就是 “Hand” 的模型。这个模型当中有一个 “deck” 属性,表明了这一手牌当中的数据都只能从这个 “deck” 属性当中获取。这个模型是一个类,您或许注意到了,当我们学习 Swift 的时候,被告知类是不好的东西。我们应该尽可能地使用结构体,因此过一会儿我们会将这个类改变为结构体,不过我们会先以 Objective-C 的心态开始。

我们的 Hand 类拥有 “deck” 的数据,它还持有了一个类型是 Card 的数组,来保存其当中所拥有牌的数据。

为了实现填充表视图的方法,我需要知道这里有多少张牌,以及牌所处的位置是几何。

var numberOfCards: Int {
    return cards.count
}

func cardAtPosition(index: Int) -> Card {
    return cards[index]
}

第一个使用的是计算属性,第二个使用的是返回 Card 的方法,因此我就可以知道牌的数量,以及某张牌所在的位置。我们可以支持表视图数据源了。我们需要添加支持增加、删除和移动牌位置的方法。

func addNewCardAtIndex(index: Int) {
    insertCard(deck.nextCard(), atIndex: index)
}

private func inserCard(card: Card, atIndex index: Int) {
    cards.insert(card, atIndex: index)
}

func deleteCardAtIndex(index: Int) {
    cards.removeAtIndex(index)
}

func moveCard(fromIndex: Int, toIndex: Int) {
    let cardToMove = cards[fromIndex]
    deleteCardAtIndex(fromIndex)
    insertCard(cardToMove, atIndex: toIndex)
}

当然,您可以就此停手,我不会怪你什么的。然后,Swift 正在不断的发展,我们需要跟上它的节奏。

这样想想吧:当 Swift 发布的时候,我们都不知道怎样做才是对的。整个社区都是在不断的学习、不断的尝试。

将类变为结构体 (6:40)

首先,我需要将模型从类变为结构体。为了让它更符合 Swift 的风格,它需要是值类型,而不是引用类型。使用引用类型的话,我们需要很好的追踪应用当中的变化。因为有很多对象都会指向这个引用类型,因此这些对象都有能力去修改它。而使用值类型的话,这就不可能发生了,因此我会将这个 Hand 模型从类变为结构体。

不幸的是,如果你使用结构体的话,所有变更了属性值的方法都必须是突变的 (Mutating)。我们在视图控制器当中设置的 Hand 属性现在也必须是变量了,因为它可能会被改变。

这就是结构体和类之间的区别。如果 Hand 仍然是类的话,那么我们改变了它的属性,Hand 仍然可以是常量,它里面的属性可以随意变化。然而,由于值类型的语义,Hand 本身就必须要是变量了。

接着,我需要努力消除突变,以便让其更『函数化』。我们接下来所做的就是函数式当中『有趣的部分』了。如果我们不再变更 Hand 的值,那么这些方法就必须要返回一个新的 Hand 实例,因此 addNewCard(_:) 现在需要返回一个 Hand 的实例。

func addNewCardAtIndex(index: Int) -> Hand {
    return insertCard(deck.nextCard(), atIndex: index)
}

我们的私有方法 insertCard 也不能够是突变方法了,我们需要让其返回 Hand 的一个新实例。

private func insertCard(card: Card, atIndex index: Int) -> Hand {
    var mutableCards = cards
    mutableCards.insert(card, atIndex: index)
    return Hand(deck: deck, cards: mutableCards)
}

这就是移除突变的典型方法。我们将要创建一个不可变牌的本地可变拷贝,或者通过向不可变数组中插入牌来创建一个添加后的数组,接着创建一个新的 Hand 实例,它使用的是一样的『一副牌』,但是里面是我们所添加的牌的新可变数组。

编辑视图控制器 (9:37)

如果我们对 Hand 新增了改变,那么我们就必须要回到视图控制器中去存储这个新值。我必须要将 hand 赋值为 Hand 的这个新实例。我不想要让其成为类方法,也不像让其成为做出修改的突变方法,因此我必须要将这个 Hand 的新实例传递回我的 hand 变量当中。

当我添加新的牌的时候,我会执行同样的事情。让我们回到我们的模型当中,然后看一看删除应该是什么样子的。

func deleteCardAtIndex(index: Int) -> Hand {
    var mutableCards = cards
    mutableCards.removeAtIndex(index)
    return Hand(deck: deck, cards: mutableCards)
}

Delete 方法做的事情是和 Add 相似的。我们创建并修改了一个可变的本地副本,接着我们使用相同的数据创建了 Hand 的一个新实例,但是这个时候牌中有元素被删除了,这就是我们所得到的。AddDelete 看起来非常相像。

然而,Move 则大有不同。让我们从非函数式版本开始。

mutating func moveCard(fromIndex: Int, toIndex: Int) {
    let cardToMove = cards[fromIndex]
    deleteCardAtIndex(fromIndex)
    insertCard(cardToMove, atIndex: toIndex)
}

这很像您在 Objective-C 中对一个数组中的两个数据进行交换。您需要将其中的数据在某个地方进行存储,然后执行转换,接下来再将存储的数据放到合适的地方。我们将需要移动的牌拿出来,然后在某个地方暂时进行存储。接着我们删除了这个牌,然后将这个牌插入到我们要保存的新位置当中。

非函数式的方法是没有任何问题的,但是我们有更函数式的版本:

func moveCard(fromIndex: Int, toIndex: Int) -> Hand {
    return deleteCardAtIndex(fromIndex).insertCard(cards[fromIndex], atIndex: toIndex)
}

在这里,我使用了这个 moveCard 函数,它会返回 Hand 的一个新实例。它仍然会继续使用 deleteCardAtIndexinsertCard,但是注意到我们使用 . 将它们链接到了一起——这个点意味着我们删除了这个牌,然后它会返回 Hand 的一个新实例,然后接着让这个新实例来完成插入牌的操作。我们没有任何的存储操作了。

为什么不这样做呢?deleteCardAtIndex 不回改变我们目前的牌集合;它创建了新的一手牌。我们既有的牌集合仍然存在,因此我创建了这个新的一手牌,接着我询问它『嗨,您能否插入一张牌?』。它反问道:『什么牌?』。幸运的是,我们当前的牌集合仍然存在,于是我们回答:『这张牌』。如果这种做法不能取悦您的话,那么您还是回去写 Ruby 或者 Java 吧!

让我们从另一个方向来优化这个示例,我们通过使用自定义单元格来改变我们的 UI 展示方式。

创建自定义单元格 (12:52)

我们要做的事情和之前也差不多,不过这让我们能够好好考虑如何创建能够备份这些信息的自定义单元格。我们的自定义单元格将会叫做 CardCellCardCellUITableViewCell 的子类。当您在处理 UIKit 层的时候,您总是会接触到各种类,因为您想要轻松地通过继承得到父类的所有功能和行为。我们的 UITableViewCell 本应是属于视图层的,但是我发现当我写这部分代码的时候,它往往会加入各种控制器层的代码。我在 Storyboard 中实现这个视图,虽然这段我在自定义 CardCell 类当中写的代码更像是控制器层的。

我需要能够访问我的两个标签。因此需要有个东西来负责存储牌的大小,另一个负责存储牌的花色。我们可以使用改进的 MVVM 架构。

刚开始,这看起来会把代码变得更复杂、更糟糕,因为我们的 Hand 视图控制器必须要访问表视图,然后还必须要访问自定义单元格。然而,这只是黎明前的黑暗而已,我们即将迎来曙光。

这个是我自定义单元格当中的 fillWidth(_:) 方法。

func fillWith(card: Card) {
    rankLabel.textColor = card.color
    rankLabel.text = card.rank.description
    suitLabel.text = card.suit.description
}

记住,控制器的任务是与模型和视图进行交互。控制器将对这个对象说:『这里是您需要填充的牌模型。我想让你根据这个牌模型,然后用正确的颜色绘制牌的花色和大小』。这样 Card 单元格就知道如何填充了。

我的视图控制器必须要确保它与正确的单元格对象类型进行交互。我们使用 guard let,我们大家都喜欢的东西,我取出重用的单元格,然后确保它是正确类型的单元格,这样它就可以使用这些东西进行填充了。

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCellWithIdentifier("cardCell", forIndexPath: indexPath) as? CardCell else {
        fatalError("Could not create CardCell")
    }
    cell.fillWith(hand.cardAtPosition(indexPath.row))
    return cell
}

有一点我很在意的小地方,我觉得 fillWidth 有点太长了,并且有点冗余,我必须要读完它,然后才知道 cardAtPosition 的涵义。如果您回到模型中然后实现一个下标语法的话,那么就可以简化这段代码的外观了:

subscript(index: Int) -> Card {
    return cards[index]
}

我直接访问 Hand 的下标元素,这样我可以看到,这个映射非常的好用。如果您不使用下标语法的话,这也不是很影响。

    cell.fillWith(hand.cardAtPosition(indexPath.row))
    // 转变为:
    cell.fillWith(hand[indexPath.row])

提取数据源 (16:39)

我们再转个方向,我将数据源从视图控制器当中提取出来。

UITableViewController 自带有一个模板,它自动实现了 UITableViewDataSourceUITableViewDelegate,但是您无需离开这里。

我们将从表视图控制器中提取数据源。我们将模型与模型之间分离出来,所以这个时候,如果您不想使用这个表格视图来显示牌的话,我们通过这个设计,您可以使用它来显示其它东西。

我的数据来源是满足 UITableViewDataSource 协议的 NSObject 子类。不幸的是,它必须是 NSObject 子类,这样才能满足我们所需要做的事情;它不能是一个基础类,也不能是结构体。它拥有模型对象的句柄。这就是我们所做的,我们将模型的管理权限下放到了这个子类当中,除此之外还有那些相关的协议方法。

这些方法曾经是用在表视图当中的,现在我们把它们放到我们的数据源当中,这让我们感觉非常良好。所有的东西现在都在这里了。

class DataSource: NSObject, UITableViewDataSource {
    private var hand = Hand()
    func addItemTo(tableView: UITableView) {}
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {}
    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {}
    
    func insertTopRowIn(tableView: UITableView) {}
    func deleteRowAtIndexPath(indexPath: NSIndexPath, from tableView: UITableView) {}
}

运行起来没有任何问题,就跟以前一样。如果我们看一下我们的视图控制器,会发现清爽很多。

class HandVC: UITableViewController {
    private varDataSource = DataSource()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = dataSource
        self.navigationItem.leftBarButtonItem = self.editButtonItem()
    }
    
    // MARK: - Action
    @IBAction private func addNewCard(sender: UIBarButtonItem) {
        dataSource.addItemTo(tableView)
    }
}

我们让控制器使用属性持有了这个数据源,因为它需要接收数据。我们在 viewDidLoad() 当中设置我们表视图的数据源,另一件我们所要做的事情就是,当有人点击加号按钮的时候,我们将这条信息传递到我们的数据源当中。我们同样必须要题型数据源它所关联的表视图是哪一个,因为我们在这个时候连接到了相应的表视图,但是数据源并不能确定是否关联。我们必须要将视图作为参数传递给数据源。

这样做的好处就是,如果我们回头看一下我们的数据源的话,这些方法中的每一个都会接收一个表视图。它只需要知道它需要处理的表视图是哪一个,这就足够了。

下面的这两个方法是我们所提取出来的用以处理 UI 的代码。这两个方法有一点点不同,所以我想要先介绍一下协议,并且将代码提取出来。

    func insertTopRowIn(tableView: UITableView) {}
    func deleteRowAtIndexPath(indexPath: NSIndexPath, from tableView: UITableView) {}

您或许会想了,您不是刚刚把它们提取出来了么,为什么我现在又要把它弄回去呢?是的,不过我们现在并不是要提取到原来的地方。我们首先先介绍一下协议。

协议的介绍 (19:20)

我打算称呼它为 SourceType。我会将 GUI 代码提取出来。我的协议源类型将需要符合我的 UITableViewDataSource,并且它持有了那些虚函数。我在此进行声明,然后添加一个协议扩展(感谢 Swift 2,这样我就可以在这里真正地实现这些方法了)。在这个协议扩展里面,我可以实现这些方法。

protocol SourceType: UITableViewDataSource {
    func insertTopRowIn(tableView: UITableView)
    func deleteRowAtIndexPath(indexPath: NSIndexPath, from tableView: UITableView)
}

extension SourceType {
    func insertTopRowIn(tableView: UITableView) {
        tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Fade)
    }
    
    func deleteRowAtIndexPath(indexPath: NSIndexPath, from tableView: UITableView) {
        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    }
}

现在我拥有了我的数据源。它是 NSObject 的子类,它需要实现 UITableViewDataSource,并且它还需要实现 SourceType。因为它实现了这个协议,因此我现在可以轻松获得我之前在协议扩展中所定义的那些功能了。

class DataSource: NSObject, UITableViewDataSource, SourceType {}

“Card” 这个单词真是无处不在啊。我似乎把我的模型对象传递得有点远了啊。我想要看一看 Hand.swift 当中的内容,我到处都使用了 “Card” 这个单词,我想要将它替换为 “item”。现在我可以把这个模型提取出来了,我可以将其转换为一个协议。

在这个我称之为 DataType 的协议中,我打算提取所有的公共方法。这是一件很好的事情,因为如果您让其他数据类型实现这个协议的话,它就可以直接使用这个提供的所有方法了。

我的模型必须能够告诉表视图数据源它有多少个项目。它还必须要能够添加项目,还需要能够删除项目,还需要能够移动项目。这个协议做了协议所需要做的一切事情,它表明:『我要做的事情是什么什么,我必须要支持的类型是什么什么』。

protocol DataType {
    var numberOfItems: Int {get}
    func addNewItemAtIndex(index: Int) -> Self
    func deleteItemAtIndex(index: Int) -> Self
    func moveItem(fromIndex: Int, toIndex: Int) -> Self
}

一旦我们创建完数据类型后,我就必须要在协议当中搞点小动作,也就是我们原先是需要返回 Hand 的地方,它们现在必须要返回 Self。因此,如果 Hand 匹配了这个协议,那么它就会返回 Hand。不管实现的数据类型是什么,它都将会返回其本身的类型,因此我们通过 Self 来捕获实现它。

struct Hand: DataType {}

由于我已经实现了这个,因此我就可以使用我的数据类型了。与其让源类型或者数据类型明确知晓 Hand 的内容,还不如让它们直接与这个数据类型进行交互。这样我就可以从我的代码库当中消除 Hand 这个词语。

protocol SourceType: UITableViewDataSource {
    var dataObject: DataType {get set}

SourceType.swift 当中,我添加了一个 dataObject,它是一个 DataType 类型。对于 Hand 这个例子来说,这里的 DataType 就是 Hand。我还让其声明了一个 getter 和 setter,因此在我的 DataSource.swift 当中,我的 dataObject 将是 Hand 类型的一个元素。

class DataSource: NSObject, UITableViewDataSource, SourceType {
    var dataObject: DataType = Hand()
    
    func addItemTo(tableView: UITableView) {
        if dataObject.numberOfItems < 5 {
            dataObject = dataObject.addNewItemAtIndex(0)
            insertTopRowIn(tableView)
        }
    }
}

松了一口气了吗?大家可以休息一下。

我们使用 “item” 这个单词来代替 “card” 这个单词。我想要在索引 0 位置处添加一个新的项目。再说一遍,我知道我的单元格的类型是正确的类型,因为我们在此之前就已经测试过了。我们同样在这里添加了 guard let,以再次检查以确保我取回的数据对象实际上是 Hand 类型的元素,因为在我试图使用这个类型的模型填充此类型的单元格之前,我必须要确保单元格的类型和模型的类型是正确的。guard let 将两者都一并检查了,如果两者通过的话,我就可以继续填充数据了。

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCellWithIdentifier("cardCell", forIndexPath: indexPath) as? CardCell else {
        fatalError("Could not create CardCell")
    }
    
    hand = dataObject as? Hand else {
        fatalError("Could not create Card Cell or Hand instance")
    }
    cell.fillWith(hand.cardAtPosition(indexPath.row))
    return cell
}

我现在对于从数据源中提取东西非常有兴趣,我要看看我还能从它当中弄出多少东西来。

if dataObject.numberOfItems < 5 {}

我记得有一种设计模式叫作『模板模式 (template pattern)』。模板模式可以让我们做一些事情,然后等到具体的情况下再完成所有的操作。

protocol SourceType: UITableViewDataSource {
    var dataObject: DataType {get set}
    var conditionForAdding: Bool {get}

SourceType 这里,我可以添加一个名为 conditionForAdding 的计算属性,它可以具体实施 numberOfItems < 5 这条语句,这让我可以回到 SourceType 当中,我可以使用这个条件来向其中添加项目,我往扩展中加入这个功能,因为它已经在协议中声明了。

var conditionForAdding: Bool {
    return dataObject.numberOfItems < 5
}

extension SourceType {
    func addItemTo(tableView: UITableView) {
        if conditionForAdding {
            dataObject = dataObject.addNewItemAtIndex(0)
            insertTopRowIn(tableView)
        }
    }
}

每一个元素都有着能够添加的条件。我很想把所有的东西都移到协议当中,但是我不能这样做,为此您可能会有点生气和奇怪。因为您这个时候已经忘记了,我们现在迈的步子太大了,在 Swift 的道路上越走越远了,我们已经忘记了我们仍然可以使用子类这种东西了。我们完全可以使用它。我知道继承有点不好,但是我们在恰当的时候使用,就会是很正确的做法。

如果我们现在要实现这个做法的话,我们可以引入 DataSource 的一个子类,我们可以将需要迁移的代码迁移到这个子类当中,使用 HandDataSource 来扩展 DataSource

class HandDataSource: DataSource {}

在我的 Hand 视图控制器当中,我添加我创建的这个子类的实例:

class HandVC: UITableViewController {
    private var dataSource = HandDataSource()

现在,我将我所有的 Hand 引用全部放到子类当中,我要做的就是通过重写添加的条件就可以了,因此我可以将其提取到父类当中,然后重写我的表视图,这样我就必须从父类中将其提取出来了:

class HandDataSource: DataSource {
    init() {
        super.init(dataObject: Hand())
    }
    
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCellWithIdentifier("cardCell", forIndexPath: indexPath) as? CardCell, hand = dataObject as? Hand else {
            return UITableViewCell()
        }
        
        cell.fillWith(hand[indexPath.row])
        return cell
    }
    
    override var conditionForAdding: Bool {
        return dataObject.numberOfItems < 5
    }
}

那么现在的父类又是什么样的呢?

var conditionForAdding: Bool {
    return false
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
    fatalError("This method must be overriden")
}

如果您问我,我可以添加东西吗?我的回答是『不,你不能』。如果你不重写表视图的话,我将会抛出一个错误。您必须要重写这两种方法才能够完成您向做的事情。

那么我该如何创建一个新示例呢?我现在有了 dataObject,它是 DataType,因此我必须要初始化它。我们没有依赖泛型就已经走了这么远了,我感觉非常良好,但是我现在还是需要引入泛型了,因为我们要初始化的必须是 DataType 这个类型。我必须这样做,只要您的类实现了 DataType ,这样您的 dataObject 就可以���您想要的类型,家下来我在这里实际做的就是调用父类的初始化方法就可以了,然后传递进一个 Hand 对象进去,并且这个对象还是实现了 DataType 的。

class DataSource: NSObject, UITableViewDataSource, SourceType {
    var dataObject: DataType
    
    init<A: DataType>(dataObject: A) {
        self.dataObject = dataObject
    }
}

class HandDataSource: DataSource {
    init() {
        super.init(dataObject: Hand())
    }
}

回顾 (27:18)

我们从表视图开始,然后以看起来更为复杂的代码结束了,但是实际上,我们已经用很多方法对其进行了简化。

我们已经分解出了不需要改变的代码,也就是那些您需要对所有表视图进行处理和重用的代码。您也不必这样做,但是我向大家展示了我是如何将代码进行迁移的,并且我留下来的都是那些需要改变的代码。这些都是您唯一需要对不同的表视图进行改变的东西。我们利用三种编程思想的优势:面向对象编程、函数式编程和面向协议编程完成了我们的改进。

千万不要限制自己说:『我只使用面向对象、我只使用函数式、我只使用面向协议』。您现在拥有了这些强大的工具,您现在可以轻松的将它们结合起来。大胆的使用它们就行了!

About the content

This talk was delivered live in September 2016 at try! Swift NYC. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Daniel Steinberg

Daniel is the author of the books ‘A Swift Kickstart’ and ‘Developing iOS 7 Apps for iPad and iPhone’, the official companion book to the popular iTunes U series from Stanford University. Daniel presents iPhone, Cocoa, and Swift training and consults through his company Dim Sum Thinking. He is also the host of the CocoaConf Podcast.

4 design patterns for a RESTless mobile integration »

close