というわけで、画面をUIViewControllerごと切り替えるのが、iOSアプリの流儀なわけですが…
切り替えた先でユーザーが何かを指定した場合に、どうやってそれを切り替え元に知らせるのか?
例えば今回なら、表示した地図でユーザーに移動先を選択してもらうって利用形態(格好つけてユースケースと言ってもいい)が考えられるけど
注意)あんま普段からカッコつけてると、自分でも気づかないうちに「EC、ソーシャルメディアとリアルを融合させたオムニチャネル活用により、個々の顧客のニーズに合わせた新たなカスタマー・ エクスペリエンス…」とか喋り出してしまうようになる。
その場合、地図表示担当のMapViewControllerから戻ってきたViewController側は、MapViewControllerコントロール下でユーザーが選んだ目的地をどうやって知ればいいのか?
MapViewController側でユーザーがおこなった操作を、ある程度ViewController側も把握したいわけで、これは前に紹介したターゲットアクションデザインパターンと同じくオブジェクト間の連携問題となります。
注意)ターゲットアクションデザインパターンがわからん人は「タップしてドン」を読みなさい。
こういったときにAppleが推奨してる作法が、デリゲートデザインパターンっす。
デリゲートデザインパターン
ターゲットアクションデザインパターンが、イベントが発生した時用の
連携オブジェクト(ターゲット)
連携メソッド(アクション)
の2つを連携先に登録するやり方だったのに対し、デリゲートデザインパターンは、最初に
イベントA発生時は、連携オブジェクトのメソッドAを呼び出す
イベントB発生時は、連携オブジェクトのメソッドBを呼び出す
・・・
といった取り決めをしておいて
連携オブジェクト(今回ならViewController)
だけを連携先(今回ならMapViewController)に登録するやり方です。
この連携オブジェクトのことをデリゲート(delegate:イベントに対する動作を委譲する相手)と呼びます。
なので、デリゲートとなる連携オブジェクトはあらかじめ取り決められたメソッドを装備してないダメってことになります。
例えば次のような取り決めをしたならば
取り決め:
MapViewControllerは地図画面でユーザーが目的地をタップしたら
func selectDistination(name:String);
というメソッドで、連携先に目的地の名前を知らせる。
MapViewController側から見てデリゲートとなるViewController側ではselectDestination(name:)メソッドを用意しておく必要があるわけです。
class ViewController: UIViewController { ・・・ func selectDestination(name:String) {・・・} }
でもってMapViewControllerでは、デリゲートを特定するためにViewController型のプロパティを用意し外部から指定可能にしておく。プロパティの名前はなんでもいいんだけど、わざわざ関係のない名前にしてもしょうがないので普通はdelegateって名前にします。
class MapViewController: UIViewController {
var delegate:ViewController?
・・・
}
注意)ViewController後ろの?の意味がわからん人は「アンラップしてチン♪」を読みましょう。
こうしておいてMapViewController作った時に、次のようにViewControllerをdelegateプロパティに設定してやれば
class ViewController: UIViewController {
・・・
@objc func showAsView() {
let mapViewController = MapViewController()
mapViewController.delegate = self
self.present(mapViewController, // mapViewControllerに切り替え
animated: true) // 切り替えはアニメーションさせる
}
あとはMapViewController側で地図画面タップされた時に、目的地名を決めてselectDestination(name:)を呼び出してやればいいわけですよ。
class MapViewController: UIViewController { var delegate:ViewController? ・・・ @objc func tap(tapGestureRecognizer:UITapGestureRecognizer) { let location = tapGestureRecognizer.location(in: self.view) ↓とりあえず、タップ位置を名前にする self.delegate?.selectDestination(name:"\(location)") }
注意)UITapGestureRecognizerが何かわからん人は「タップしてドン」を読みましょう。
こうするとViewControllerは、MapViewControllerの地図画面上でユーザーが指定した目的地を知らせてもらえることになるわけです。
class ViewController: UIViewController { ・・・ func selectDestination(name:String) { print(name) ←コンソールに名前を出力 } }
サンプル:
http://tetera.jp/xcc/book-sample/delegate-pre.zip
ちなみに
"\(location)"
というように、文字列中に \( (バックスラッシュとオープンバーレン)と ) (クローズバーレン)で挟んでプロパティ名を書くと、そのプロパティの値が文字列として設定されます。結果として、例えばlocationのx、yに[100, 200]が設定されていたら
"(100.0, 200.0)"
て感じの文字列になり、これがコンソールに出力されます。\(プロパティ名)は、どこでも使えるので、こんな感じでもいい
"location は \(location) です"
この場合、
"location は(100.0, 200.0)です"
という文字列になる。
ただ〜、これだとまるで汎用性がないわけでして…
MapViewControllerのdelegateプロパティに指定できるのはViewControllerオブジェクトか、その派生オブジェクトしかできないってことになっちゃうんですな。
で、そこらへんに汎用性を持たせるために、Appleが提唱するデリゲートデザインパターンでは「これこれのイベントが発生したら、連携先オブジェクトの、このメソッドを呼び出す」という取り決めを、プロトコルとして表現しておき
protocol MapViewControllerDelegate {
func selectDistination(name:String)
}
注意)プロトコルがわからん人は「重箱の隅をつつくようにネチネチと進めてみる」を読みましょう。
連携先は、このプロトコルを採用するものとする。
class ViewController: UIViewController, MapViewControllerDelegate {
・・・
func selectDistination(name:String) {
print(name)
}
}
ということにしたんですな。
こうすると、MapViewController側はViewControllerではdelegateプロパティとして、ViewControllerじゃなくプロトコルであるMapViewControllerDelegate型を指定することができ
class MapViewController: UIViewController {
var delegate:MapViewControllerDelegate?
・・・
サンプル:
http://tetera.jp/xcc/book-sample/delegate.zip
MapViewControllerDelegateを採用したオブジェクトなら、どれでも指定可能になるわけです。
なので、例えば次のようにして目的地選択専用のオブジェクトを用意することだってできる。
// MapViewControllerDelegateを採用した別クラス class AltDelegate : MapViewControllerDelegate { func selectDestination(name:String) { print("タップ位置は" + name + "です") } } class ViewController: UIViewController, MapViewControllerDelegate { ・・・ // 別のデリゲートオブジェクト let altDelegate = AltDelegate() @objc func showAsView() { let mapViewController = MapViewController() mapViewController.delegate = altDelegate self.present(mapViewController, // mapViewControllerに切り替え animated: true) // 切り替えはアニメーションさせる }
self、altDelegate、どちらでもdelegateに設定可能。
サンプル:
http://tetera.jp/xcc/book-sample/delegate-alt.zip
こんな感じでプロトコルとその採用を導入することで、かなり汎用性が出てくるわけです。これがデリゲートデザインパターン。
あと、慣例としてデリゲート側プロパティにはweakをつけます。
class MapViewController: UIViewController {
weak var delegate:MapViewControllerDelegate?
これは、自分はdelegateに指定されたオブジェクトを所有しないという意思表示です。逆に言えばweakをつけてない場合は、delegateに指定されたオブジェクトはMapViewControllerオブジェクトに所有されることになり、MapViewControllerオブジェクトが存在する限り、勝手に消滅したりはできなくなります。
これ自体は、MapViewControllerにとってありがたい事なんだけど、一点だけ懸念事項があって…
それはdelegateで指定されるオブジェクトが、MapViewControllerオブジェクト側を同じようにプロパティで所有してた場合です。
こうなると互いに所有しあって、いつまでもオブジェクトが消滅できなくなる可能性が出てくるんですな。
オブジェクトは用が済めば静かに退場するのが礼儀というものなんですが、互いに所有しあってると、どちらも消滅しようとしても消滅できなくなるんですよ。
オブジェクトを用意できる最大数はiPhoneの機種によって違うけど、いずれにせよ限界はあるわけで、古参がいつまでも居座り続けるのは良くないです。
ここら辺を防ぐためにweakをつけます。
で、weakをつけたプロパティは設定されているオブジェクトが消滅するときはnilが設定されることになるので、利用するときに毎回アンラップして使うのが安全ってことになります。
これはすでに実装済み。
class MapViewController: UIViewController {
・・・
@objc func tap(tapGestureRecognizer:UITapGestureRecognizer) {
let location = tapGestureRecognizer.location(in: self.view)
self.delegate?.selectDestination(name:"\(location)")
}
最後に、weakをつけられるプロパティには条件があって、オブジェクトでないとダメなんですな。そのためプロトコルを定義するときにAnyObjectを採用します。
protocol MapViewControllerDelegate : AnyObject {
func selectDistination(name:String)
}
これでプロトコルMapViewControllerDelegateはクラスでしか採用できなくなり、結果、MapViewControllerDelegate型は必ずオブジェクトが設定されることが保証されweakがつけられるようになります。
もっとも、このプロトコルをObjective-Cでも利用できるようにしたいなら、その点を保証するためのキーワード@objcをつけることになり、このキーワードには暗黙裡にAnyObjectを継承させる指定も含まれるので
@objc protocol MapViewControllerDelegate {
func selectDistination(name:String)
}
と書くのがよく見られる書き方ってことになります。
サンプル:
http://tetera.jp/xcc/book-sample/delegate-weak.zip
というわけで、これでAppleの3大オブジェクト連携手法が出揃ったわけですな。
ターゲットアクションデザインパターン
通知
デリゲートデザインパターン
ここら辺を理解できたら、大概のサンプルは何やってるかわかるんじゃないかと思われ。あとはストーリーボードってことになるけど、これはまた次回。