Cocoa / UIKit Framework で Swift 開発する時、わりとよくある不満?の一つは、Cocoa は当時動的な言語 Objective-C のために作ったフレークワークのため、動的制御の思想があっちこっちで見られるが、Swift は静的な言語なので Cocoa フレームワークを使う時時々気持ち悪いコードも見られます(少なくとも筆者はかなり気持ち悪いと思ってます…)。
例えば UIButton がまさにその代表格です。ボタン動作は全てセレクターで定義され、例えば一つボタンを追加したいだけの場合でも、ボタン動作を別メソッド(例えば func buttonTapped(sender: UIButton) とか)で定義しなければなりません。一連のボタン群ならこの手法では動作が全てまとめられて switch 文で tag で動作分岐させればコードとしてスッキリすることもありますが、逆に非常に単純な動作を行って欲しいっていうことの時、わざわざ別メソッドを作るのも面倒ですし、そもそも論として静的言語の Swift に Runtime に動作が決まる Selector でボタンを作るのは非常に気持ち悪い以下略。なのでまあ UIButton に動作をあらかじめ埋め込んでおく Callback 方式を対応して欲しいとずっと昔から思ってましたが未だに公式がそれをやってくれてないので、自分でなんとかするしかないのです。
というわけで早速 UIButton のサブクラスを作ります:
import UIKit
class CallbackButton: UIButton {
private var action: (() -> Void)?
init(frame: CGRect, action: (() -> Void)? = nil) {
self.action = action
super.init(frame: frame)
self.addTarget(self, action: "tapped:", forControlEvents: .TouchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setAction(action: () -> Void) {
self.action = action
}
func tapped(sender: CallbackButton) {
self.action?()
}
}
まあこれで非常にわかりやすいと思いますが、UIButton と同じように CGRect から形を作り、その時にすぐにでも action を設定できますが、もし諸事情によりそれができない場合(親クラスがまだ init されてない時など)は後からでも setAction で定義できます。そしてまあここは仕方ないのですが一応タップ時のメソッドを定義し、その時に action?() を実行するようにします。
それではこれを使うとどれくらいスッキリするかというと、例えば適当にとある UIView クラスにボタンを追加するとしましょう。通常の UIButton を使う場合は下記のようになります:
class SomeView: UIView {
var a = 1
override init(frame: CGRect) {
super.init(frame: frame)
let button = UIButton(frame: CGRectZero)
button.addTarget(self, action: "tapped:", forControlEvents: .TouchUpInside)
self.addSubview(button)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func tapped(sender: UIButton) {
self.a *= 2
}
}
そして Callback 方式を使うとこのようになります:
class SomeView: UIView {
var a = 1
override init(frame: CGRect) {
super.init(frame: frame)
let button = CallbackButton(frame: CGRectZero) { () -> Void in
self.a *= 2
}
self.addSubview(button)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
とこんな風に、余分な tapped: メソッドを使わなくて済むのです。
まあ何より、Selector を使わずに済むってのが一番大きいじゃないかと思うんですね、Swift の場合 Selector の補完とかないし、結構「:」を必要な時に入れ忘れたり逆に要らない時に入れちゃったりすることによる Runtime Error 多いんですよな…