Tryswift daniel steinberg cover

関数型プログラミング、プロトコル志向プログラミング、オブジェクト指向プログラミングの優れたテクニックを取り入れる

Swiftでアプ���ケーションを書くということは単にObjective-Cで書かれたアプリをSwiftに書き換えるだけではありません。Swiftという言語の機能や哲学を深く理解する必要があります。try! Swiftで話されたこの講演では、標準的なMVC構成でテーブルビューを持つObjective-Cで書かれたアプリケーションをSwiftに書き換えます。単に書き換えるだけでなく、Swiftらしいコードにするために、関数型プログラミングやオブジェクト指向、デザインパターン、プロトコル志向プログラミングの考え方を上手に組み合わせます。


Swiftを始める (0:00)

Swiftを始めるにあたって、たいていの場合は既存のObjective-Cのコードを単に書き換えるだけということが多いでしょう。これからお見せするのは、みなさんご存知のプログラミングのパラダイムである、オブジェクト志向プログラミング、関数型プログラミング、プロトコル志向プログラミングの手法をすべてミックスした、よりSwiftらしいコードを書くためのテクニックです。

それでは簡単なテーブルビューの例を見ていきます。多くの人がやるようにXcodeを開いてテンプレートを選んで(本当はそうしない方がいいのですが)始めます。最初にとりあえずコントローラとモデル、そしてビューがあるだけのコードを考えてください。

手始めに、ますこのコードを単純にSwiftに変換していきます。ビューをコードかStoryboardで作成します。

モデルはこのようになっています。ひとそろいのカードから5枚手札として配られます。これを”hand”(手札)とします。

I’ll start with a Table View example you were raised with. If you open Xcode and you do the template (shame on you), you start with code where you have a controller, amodel, and a view. Initially, I translate this into Swift, so I create my view or the storyboard as we do. For the model, I’m going to use a deck of cards and deal out five cards, and I’ll call that my “hand”.

手札には数字と種類が書いてあります。これをデータ構造として表す必要があります。このモデルとビューをコントローラを使って管理することになりますね。コントローラはHandViewControllerとします。UITableViewControllerのサブクラスです。典型的なコードとしてはこんなものでしょう。このコードを改善していきます。多くの人がやるのと同じように、ビューコントローラはモデルについて多くの責務を負っています。(それはあまり良くない設計とされています。)

ではコードを改善する方法を具体的に見ていきましょう。

基本的なビューコントローラ (1:56)

まず最初に取りかかるのはビューコントローラです。ビューコントローラのクラスは手札のビューを扱うコントローラということでHandVC(ハンドビューコントローラ)といいます。HandVCUITableViewControllerのサブクラスです。

// 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で非常に気に入っているところは、プロパティの定義の仕方です。Objective-Cと違ってどこか別のところで宣言して、違う場所で初期化するということがありません。単に「これはプロパティです」と書けばいいのです。

それではこのビューコントローラはモデルについて非常に多くの責務を負っていますので、モデルをプロパティとして持っています。「プラス」ボタンが押されたときはaddNewCard(_:)というメソッドがボタンのアクションとして呼ばれます。

どういう動きをするかというと、もし手元のカードが5枚より少なかったら、1枚カードが追加されます。カードを追加するときの処理は、必ず先にモデルにオブジェクトを追加して、その後でビューを更新します。ビューに対する処理はinsertTopRow()というメソッドにまとめてあるので、アクションメソッド自体は非常にシンプルになっています。

@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
}

記事の更新情報を受け取る

上記はテーブルビューのデータソースのメソッド2つです。最初のメソッドはテーブルビューのセクションに行がいくつあるかを返すメソッドです。この値を取得するためにモデルのメソッドを呼び出しています。モデルに「何枚カードを持っていますか?」と聞いているわけです。

もう一つのメソッドはテーブルビューのセルの項目を表示するためのメソッドです。それぞれの行でこのメソッドが呼ばれ、対応するモデルから値を取得します。ここでは数と色、そして種類を設定しています。

さて、それではカードを削除する処理を見てみましょう。下記のようにメソッドを実装します。そして先ほど追加したカードを削除します。まずモデルから削除し、その後ビューを削除します。

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)
}

カードを入れ替える場合、テーブルビューの行を移動する処理は書く必要がありません。アップルはフレームワークとしてその機能を提供してくれています。しなければいけないことは、モデルに対して「このカードとこのカードの位置を入れ替えたい」と伝えることです。ここまでがビューコントローラのコードです。ではこれからモデルの処理がどのように実装されているのか見ていきましょう。

基本的なモデル (4:45)

import UIKit

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

これは手札を表したモデルです。手札とは配られたひとそろいのカードです。このモデルはクラスとして実装されています。SwiftのコードではClassを使用するのはあまり良くないと思うかもしれません。すべてのクラスはStructであるべきでしょう。後からこの部分はStructに書き換えるつもりですが、今はObjective-Cからちょうど移行始めたところなのでこのままにしておきます。

手役のクラスではひとそろいのカードをCard型の要素を持つ配列として持っています。

テーブルビューのメソッドに対応するために、モデルには何枚のカードを持っているかと、カードの並びを返すメソッドがありです。

var numberOfCards: Int {
    return cards.count
}

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

カードの枚数については計算済みプロパティを用いることにしました。一方、カードの並びについてはメソッドで値を返すことにしました。これで、カードの位置に応じてオブジェクトを返すことができるようになりました。テーブルビューのデータソースに応答するにはこれで十分です。さらにカードの追加・削除、それと並べ替えをサポートするメソッドを見ていきます。

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は今も進化している途中です。ですので私たちも一緒に進化していくべきだと思います。

このように考えてください。Swiftが登場したとき、誰もSwiftの正しい書き方は知りませんでした。そのため、私たちはコミュニティを作り、試行錯誤しながら共に学んで行くことにしたのです。

クラスをStructに改良する (6:40)

まずはじめに、モデルのクラスをStructに変更したいと思います。よりSwiftらしいコードにするためには、参照型よりも値型を使う方が良いのです。参照型を使っている場合はで複数の箇所で値を変更すると、参照する同じオブジェクトすべてが変わっていました。モデルを変更しているところは一か所だけではなく、いろいろなところでモデルは変更され、その変更は伝播します。いっぽう値型では変更が伝播するようなことはありません。ですので、クラスをStructに変えていくことにします。

残念なことに、Structを使用する場合、自分のプロパティを変更するメソッドにはすべてmutateキーワードを付けなければなりません。さらにビューコントローラのhandプロパティは定数ではなくvar(変数)として定義する必要があります。理由は、そのプロパティが変更されるからです。

これがStructとクラスの違いです。handプロパティがクラスであったならば、letのままでもよかったです。プロパティはletでもそのオブジェクトが持つ値は変更することができます。しかし値型のセマンティクスにおいては、hand自身をvarとしなければなりません。

ということで、次のステップでは、より関数型プログラミング的なアプローチによって、mutatingを無くしたいと思います。関数型プログラミングの楽しいところです。もしhandを変更できないとしたらどうなるでしょうか。値を変更する代わりに、新しいHandインスタンスを作って返すようにメソッドを変更することになります。つまり、addNewCard(_:)メソッドは新しくHandインスタンスを作成して返すように変更します。

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

それによってプライベートメソッドのinsertCardmutatingではなくなりました。代わりに新しいHandインスタンスを返します。

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

これはmutatingを取り除く典型的なテクニックです。イミュータブル(不変)なカードの配列のミュータブルなコピーをメソッドローカルで定義します。そして追加されるカードオブジェクトを配列に追加し、嵐くHandオブジェクトを作ります。このとき、Handオブジェクトには先ほどのカードの配列を渡して、同じ手札を持つオブジェクトとして返します。

ビューコントローラに変更を加える (9:37)

handオブジェクトに変更を加えたら、ビューコントローラのプロパティにも新しい値で保持しなければなりません。クラスを使っていた場合と違って、値を変更するためには新しいインスタンスをプロパティに代入する必要があります。

もちろんカードを追加したときにも同じことが必要です。カードをモデルから削除する場合も同様なので、削除する処理を例としてコードを見ていきましょう。

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

deleteメソッドの処理はaddメソッドがやってることとほとんど同じです。ミュータブルなローカルコピーを作成して値を変更し、Handインスタンスを新しく作成し、同じ手札を持足せます。ただ一つ異なるのは、手札の要素が減っているということです。それ以外は、addメソッドとdeleteメソッドは非常によく似ています。

しかし、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の2つのメソッドを使っていますが、2つのメソッドを.(ドット)で繋いでいます。メソッドをチェインしています。

この部分は、まずカードを削除し、削除された手札を持つHandオブジェクトを新しく作って返し、そのオブジェクトに対して、カードを挿入する、という意味になります。ここでは一時的なオブジェクトをどこかに保存したりはしていません。

どうしてこのようにできるのでしょうか?deleteCardAtIndexメソッドはプロパティのカード配列には変更を加えません。新しくカードが減った手札を作って返します。プロパティのカード配列はそのままです。そして、新しく作られた手札オブジェクトに対して、「カードを追加してください。」と命じます。すると「どのカードですか?」と手札が尋ねます。ちょうど元の手札の配列はそのまま残っているので「このカードです」と答えることができます。もしこのやり方が気にいらないと思うなら、RubyかJavaで書いたほうがいいですね。

次に、カスタムセルを作る処理について、どのように変更していけばいいのか見ていきましょう。

カスタムセルを作る (12:52)

すでにあるものと同じものを作っていくのですが、カスタムセルを作るということの背景について考えてみましょう。カスタムセルはCardCellという名前です。CardCellUITableViewCellのサブクラスです。UIKitを用いて何かを作っていくときは、ほとんどの人がクラスを使おうとします。なぜなら標準のもともと実装されている挙動を簡単に利用したいからです。

UITableViewCellのサブクラスに書かれているコードは「ビュー」の責務に関係するコードだとみなさんは考えているかもしれません。しかし、ほとんどのコードは「コントローラ」のコードなのです。ビューをストーリーボードで作成したとしても、カスタムのカードセルクラスのコードはやはりコントローラのコードだといえます。

これから示すのは、セルが持っている2種類のラベルをMVVMを用いて改善する方法です。ラベルのうち一つは種類を表示するラベルで、もう一つは数を表示するラベルです。

最初はこんな感じになります。前より悪くなってるように見えるかもしれません。ハンドビューコントローラはテーブルビューと関係していますし、カスタムセルとも関係しています。しかし、これは後から良い形にしていくので安心してください。

こちらがカスタムセルが持つfillWith(_:)メソッドです。

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

コントローラの役目はモデルの変更をビューに反映させることです。このコントローラはセルのオブジェクトに対して、「これがビューに表示すべきカードです。このカードを受け取って、数と種類を適切な色で表示してほしい。」とビューに命じます。カードセルはどのようにデータを表示するかを知っています。

ビューコントローラでは適切なセルに対してメソッドを呼んでいるか保証する必要があります。みんな大好きな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
}

これは細かいことですが、fillWithメソッドはあまりキレイではないですね。またcardAtPositionメソッドも読みにくいと感じます。サブスクリプティングでモデルのデータにアクセスするようにしたら、シンプルで読みやすくなります。

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

手札が持つカードに対して、操作をしていることが明らかにな李、とても分かりやすくなったと思います。もしサブスクリプティングを使っていなければ、これはちょうど良い例だと思います。

    cell.fillWith(hand.cardAtPosition(indexPath.row))
    // turn into:
    cell.fillWith(hand[indexPath.row])

データソースを分割する (16:39)

今度は別の改善点を見ていきます。次はデータソースをテーブルビューコントローラの外に出していきたいと思います。

UITableViewControllerをテンプレートを用いて作成すると、自動的にUITableViewDataSourceUITableViewDelegateが実装された状態になります。しかし、それをそのまま使うべきではありません。

データソースのコードはテーブルビューコントローラの外に出していきましょう。モデルについての関心ごとを分割して、テーブルビューの責務のコードとそうでないコードが明確に分かれるように設計します。そうすることによって、テーブルビューをひとそろいのカードを表示すること以外にも使えるようにできます。もちろんコードを変更することなくです。

新しいデータソースのクラスはNSObjectのサブクラスでUITableViewDataSourceプロトコルに準拠しています。残念なことに、NSObjectのサブクラスにしなければなりません。スーパークラスを持たないクラスや、Structにすることはできません。モデルオブジェクトを取り扱うので、モデルに関係する責務のコードとメソッドをすべて移してくることができます。

下記が、テーブルビューで使われるメソッドです。もともとテーブルビューコントローラにあったものがデータソースに移動してきました。とても良い感じです。

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()メソッドでテーブルビューのデータソースとしてこのオブジェクトをセットします。あとやらなければいけないことは「プラス」ボタンのアクションの処理だけです。ここで、データソースにメッセージを受け渡します。データソースオブジェクトはテーブルビューのデータソースであることを思い出してください。テーブルビューはコントローラが保持していますが、データソースは持っていません。なので、テーブルビューを引数で渡します。

このようにデータソースを変更して良買った点は、データソースのメソッドが必要に応じてテーブルビューを引数として取るようになったことです。テーブルビューへの依存を適切に分けることができました。

下記の2つのメソッドは、UIを更新するコードを移してきました。このメソッドは上記で見てきた種類のメソッドとは少し異なります。ここで、プロトコルを導入して、このメソッドをまた別のところに持っていきたいと考えています。

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

これまで「コードを別の具体的なオブジェクトに分割して移してきた、ならここで抽象化するのではないか」と考える人がいるかもしれません。その通りです。このメソッドは別のところに抽象化したいと思います。そのためにプロトコルを導入しましょう。

プロトコルを導入する (19:20)

新しくSourceTypeというプロトコルを作ります。ここにGUIのコードを移そうと思います。SourceTypeプロトコルは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”という変数だらけです。”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オブジェクトが入ります。プロパティとして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)
        }
    }
}

ここまでついて来れてますか?いつでもストップと言ってくれて大丈夫ですよ。

“card”という変数の代わりに”item”を使うことにしました。そこに新しいアイテムを先頭に追加しようとするとき、セルが正しい型であることは明らかです。なぜなら前の段階でチェックを追加したからです。同様のチェックを、データオブジェクトが正しい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 {}

デザインパターンにテンプレートパターンというものがあります。テンプレートパターンを使うと、ある操作の前後に特定の処理を実行することができるようになります。

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

上記のコードはSourceTypeプロトコルの定義です。ここにconditionForAddingという計算済みプロパティを追加しました。ここにはアイテムの数が5より少なかったらというnumberOfItems < 5の処理として実装されるべきです。しかし、この場合はSourceType自体にこの条件をエクステンションとして実装することができます。SourceTypeがプロトコルだからですね。

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

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

こうすることで、要素を追加しようとするときはいつでも、この条件を満たしているというとことが保証されます。もっと何もかもすべての処理をプロトコルに持ってきたいところなのですが、この辺りにしておきます。ここまででSwiftらしいコードにするために本当にたくさんのことをしてきました。しかし、もしかしたらサブクラスの問題を残したままであることをみなさんは気にしているかもしれません。これはあまり良くないやり方かもしれませんが、これが適切であると考えるときはこのテクニックを使うことができます。

Every element must have this condition for adding. I’d love to move everything else up into the protocol too, but I can’t, and at this point you might get angry because you’ve forgotten, we’ve done so much Swift we’ve forgotten that we can still introduce subclasses. We’re allowed to do that. I know they’re a little bad, but we can do that when appropriate.

ここで、DataSourceのサブクラスを導入して、コードを移してきたいと思います。新しくDataSourceを継承したHandDataSourceを作ります。

class HandDataSource: DataSource {}

ハンドビューコントローラでは、データソースのインスタンスをこのサブクラスを使うようにします。

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

そして、すべてのHandのオブジェクトをこのサブクラスに移します。conditionForAddingメソッドとテーブルビューセルのメソッドをオーバーライドする必要があります。スーパークラスではこれらのメソッドは不要になるので単なるスタブにします。

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")
}

何でもいいから実装した方がいいと言う方がいるかもしれません。しかし、私はこのメソッドはスタブのままにしておくのがいいと思います。もしテーブルビューのメソッドがオーバーライドされなかったらfatalErrorでクラッシュします。早めにクラッシュさせた方がいいと考えています。

どのようにインスタンス化するのがいいでしょうか?dataObjectDataTypeなのでそのオブジェクトを渡して初期化するようにしました。ジェネリクスは使いませんでした。その方が良いと思っていますが、ジェネリクスを使うこともできます。そうするならDataTypeをジェネリクスの境界として設定します。こうすることでdataObjectDataTypeに準拠していることが保証できます。サブクラスではスーパークラスのイニシャライザを呼びHandオブジェクトを渡します。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

2016年9月のtry! Swift NYCの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。

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