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
(ハンドビューコントローラ)といいます。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で非常に気に入っているところは、プロパティの定義の仕方です。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)
}
それによってプライベートメソッドのinsertCard
もmutating
ではなくなりました。代わりに新しい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
インスタンスを作って返すように変わりました。依然として位置を入れ替える処理ではdeleteCardAtIndex
とinsertCard
の2つのメソッドを使っていますが、2つのメソッドを.
(ドット)で繋いでいます。メソッドをチェインしています。
この部分は、まずカードを削除し、削除された手札を持つHand
オブジェクトを新しく作って返し、そのオブジェクトに対して、カードを挿入する、という意味になります。ここでは一時的なオブジェクトをどこかに保存したりはしていません。
どうしてこのようにできるのでしょうか?deleteCardAtIndex
メソッドはプロパティのカード配列には変更を加えません。新しくカードが減った手札を作って返します。プロパティのカード配列はそのままです。そして、新しく作られた手札オブジェクトに対して、「カードを追加してください。」と命じます。すると「どのカードですか?」と手札が尋ねます。ちょうど元の手札の配列はそのまま残っているので「このカードです」と答えることができます。もしこのやり方が気にいらないと思うなら、RubyかJavaで書いたほうがいいですね。
次に、カスタムセルを作る処理について、どのように変更していけばいいのか見ていきましょう。
カスタムセルを作る (12:52)
すでにあるものと同じものを作っていくのですが、カスタムセルを作るということの背景について考えてみましょう。カスタムセルはCardCell
という名前です。CardCell
はUITableViewCell
のサブクラスです。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
をテンプレートを用いて作成すると、自動的にUITableViewDataSource
とUITableViewDelegate
が実装された状態になります。しかし、それをそのまま使うべきではありません。
データソースのコードはテーブルビューコントローラの外に出していきましょう。モデルについての関心ごとを分割して、テーブルビューの責務のコードとそうでないコードが明確に分かれるように設計します。そうすることによって、テーブルビューをひとそろいのカードを表示すること以外にも使えるようにできます。もちろんコードを変更することなくです。
新しいデータソースのクラスは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でクラッシュします。早めにクラッシュさせた方がいいと考えています。
どのようにインスタンス化するのがいいでしょうか?dataObject
はDataType
なのでそのオブジェクトを渡して初期化するようにしました。ジェネリクスは使いませんでした。その方が良いと思っていますが、ジェネリクスを使うこともできます。そうするならDataType
をジェネリクスの境界として設定します。こうすることでdataObject
はDataType
に準拠していることが保証できます。サブクラスではスーパークラスのイニシャライザを呼び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によって撮影・録音され、主催者の許可を得て公開しています。