UIResponderをつかったアクションの伝播.swift
extension UIResponder {
func find<Responder>(_ returningClass: Responder.Type) -> Responder? {
var responder: UIResponder? = self
while responder != nil {
if let responder = responder as? Responder {
return responder
}
responder = responder?.next
}
assertionFailure()
return nil
}
}
protocol SampleResponder {
func onTap(text: String)
}
class SampleViewController: UIViewController {
}
extension SampleViewController: SampleResponder {
func onTap(text: String) {
print(text)
}
}
class TestLabel: UILabel {
func initialize() {
let recoginizer = UITapGestureRecognizer(target: self, action: #selector(self.onTap))
addGestureRecognizer(recoginizer)
}
@objc func onTap() {
find(SampleResponder.self)?.onTap(text: text ?? "")
}
}
なんかこんな感じで Responder Chain をたどって目的の XxxResponder を探し出してメソッド呼び出しの形式でアクションを通知する仕組みの一例。
Responder chain については https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events を参照。
以下駄文。
SampleResponder は onTap メソッドを持っているが、これを良いとするか悪いとするかは絶妙だと思う。Responder を UI イベントの受け取り先とするなら問題ないが、もっと広範な処理の移譲先とするなら例えば zoom(text:) とかのほうが適切かもしれない。
find(SampleResponder.self)?.onTap(text: text ?? "") を見たときに初見で意図がわかるのか? と言われるとけっこう絶妙だと思う。find から Responder Chain を連想できないので。しかし定義に飛べばやってることは一目瞭然なのでセーフだと思っているが、もっと良い API があるかもしれない。
SampleResponder.find(in: self)?.onTap(text: text ?? "") のようにできれば良いと思うんだけど、ちょっと実装できなさそうだった。protocol 自身は static method を持てない。
find を findInResponderChain に改名したほうが、メソッド名長くても意味が通りやすくて良いかもしれない。
OOUI においてはオブジェクトがインタラクションを持つので、例えば「ユーザーオブジェクトのインタラクション」を UserObjectResponder で規定して、その実装を Responder Chain の上層に投げるという方針が考えられる。ただし、UserObjectResponder が要求するすべてのメソッドを単一のオブジェクトがカバーできるとは限らない。(たとえばユーザープロフィールの表示は UINavigationController で対応できても、ユーザーの通報は AppDelegate のほうが適切、みたいな状況)こういうときに「より上位のオブジェクトに対応を移譲する」仕掛けがあったほうが良い。
protocol UserObjectResponder {
func open(user: User)
func report(user: User)
}
extension UINavigationController: UserObjectResponder {
func open(user: User) {
let vc = UserProfileViewController(user: User)
pushViewController(vc, animated: true)
}
func report(user: User) {
next?.find(UserObjectResponder.self)?.report(user: User)
}
}
next を使うともちろん動作するけど、findの内部仕様まで把握した動かし方なのでわかりにくいといえばわかりにくい。Swift の protocol は optional method に対応していないので、個別でごにょごにょしないといけないためそれもそれでめんどくさい。next を差し込み忘れると途端に無限ループするのも危うい。といってイマイチいい方法はみつからない。
ちなみに protocol を挟まない方法ならこちらの記事が参考になる。
http://hagmas.github.io/swift/ios/uikit/2015/12/23/nil-target-action-with-swift/
解説されているコードは古いので修正するとこんな感じになる。
extension UIResponder {
func sendActionViaResponderChain<A, B, C>(_ action: (A) -> (B) -> C?, arguments: B) -> C? {
if let casted = self as? A, type(of: self) == A.self {
return action(casted)(arguments)
} else if let nextTarget = self.next {
nextTarget.sendActionViaResponderChain(action, arguments: arguments)
}
return nil
}
}
動作チェックまではしていない。
おわり。