はじめに
SwiftによるViewController間での値の受け渡しには、Protocolによるdelegate
を使用して行うことが良いとされています。
学習の振り返りも兼ねて、実装手順やその思考を残しておくことにしました。
なぜdelegateを使用するのか?
様々な理由があるとは思いますが、私は次の点が重要と考えています。
- 処理を他に委任することで、ファイル間の依存関係を減らすことができる
これは、ViewControllerなどのClass間の結び付きを少なくした方が良いよね、という思想からきているようです。
どういうことなのかは実装しながら見ていきましょう。
概要
- TableViewに表示されている文字列を、別のViewControllerに渡して表示する
- 画面遷移はNavigationControllerを使用する
こんな感じ(プレビュー) |
---|
環境
Xcode 12.4
実装
Viewの準備
ViewComtrollerを跨いで値を渡したいので、2画面を作ります。
- LabelとButtonを持つView(
ViewController
) - TableViewを持つView(
SecondViewController
) - NavigationControllerを設置
LabelとTableViewはIBOutlet
、ButtonはIBAciton
でそれぞれ接続しておきましょう。
画面遷移を実装する
画面遷移はNavigationController
を使用し、コードで実装します。
ではさっそくViewControllerファイルに、Secondへ画面遷移するためのコードを書いていきます。
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var stringLabel: UILabel! {
didSet {
stringLabel.text = "まだ選択されていません"
}
}
@IBAction func tappedButton(_ sender: UIButton) {
let secondStoryboard = UIStoryboard(name: "Second", bundle: nil)
let secondVC = secondStoryboard.instantiateInitialViewController() as! SecondViewController
let nav = self.navigationController!
nav.pushViewController(secondVC, animated: true)
}
}
はい、これでボタンを押して画面遷移できるようになりました。
この辺りの実装は雑になってしまってますが、ご容赦ください・・・
TableViewに値を持たせる
import UIKit
class SecondViewController: UIViewController {
@IBOutlet weak var tableView: UITableView! {
didSet {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.delegate = self
tableView.dataSource = self
}
}
let dataStrings = ["First", "Second", "Third", "Another", "More"]
}
extension SecondViewController: UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataStrings.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = dataStrings[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("\(dataStrings[indexPath.row])のセルがタップされました")
}
}
こんな感じでdataStrings
を定義して、cellにString型の文字列を表示させました。
didSelectRowAtでは一旦、そのセルが持つStringを出力するようにしています。
左:遷移前 / 右:遷移後 |
---|
値の受け渡しのためのProtocolを定義する
いよいよProtocolを書いていきます。
今回はTableViewCellが持つString型の文字列を渡すための記述をしていきます。
まずProtocolを書くswiftファイルを作成します。ファイル名はToPassDataProtocol
とします。
(プロトコルってどういうファイル名にするのが適切なんだろうか・・・?)
import Foundation
protocol ToPassDataProtocol: class {
func dataDidSelect(data: String)
}
このプロトコルを準拠させたクラスに、String型のdata
を引数として受け取るdataDidSelect
というメソッドの処理を記述することになります。
delegateを定義
// 一部抜粋
class SecondViewController: UIViewController {
let dataStrings = ["First", "Second", "Third", "Another", "More"]
// ↓SecondViewController内にこの行を追加
weak var delegate: ToPassDataProtocol? // 処理を任せる相手を保持する
}
delegateを定義します。
セルをタップした時に値を渡す
という処理を実行したいので、tableView(didSelectRowAt)を持つSecondViewControllerファイルに記述しています。
受取側をProtocolに準拠させる
Protocolで値を受け取るために準拠させます。
class ViewController: UIViewController {
@IBOutlet weak var stringLabel: UILabel! {
didSet {
stringLabel.text = "まだ選択されていません"
}
}
@IBAction func tappedButton(_ sender: UIButton) {
let secondStoryboard = UIStoryboard(name: "Second", bundle: nil)
let secondVC = secondStoryboard.instantiateInitialViewController() as! SecondViewController
// SecondViewControllerで定義したdelegateにself(ViewController)を設定する
secondVC.delegate = self
let nav = self.navigationController!
nav.pushViewController(secondVC, animated: true)
}
}
// Protocolを準拠させる
extension ViewController: ToPassDataProtocol {
func dataDidSelect(data: String) {
// この中の処理は一旦保留
}
}
画面遷移をするtappedButtonメソッド内でdelegateにselfを設定しています。
このself
はSecondViewControllerで定義したweak var delegate: ToPassDataProtocol?
に代入する形となっています。
処理を依頼する側(SecondViewController)から見て、処理を任せる相手(self = ViewController)を設定しているということになります。
また、Protocolに準拠させると、Protocolで記述したdataDidSelect
メソッドの実装が必須となりますが、処理の内容は一旦保留しています。
この時点では、まだ引数に何を受け取るか明確ではないからです。
値を渡す
ViewController側で値を受け取る準備が出来たので、SecondViewControllerのtableView(didSelectRowAt)の処理を書き換えます。
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let data = dataStrings[indexPath.row] // delegateに渡す定数を用意
delegate?.dataDidSelect(data: data) // ここで値を渡す
self.navigationController?.popViewController(animated: true)
}
ここで定義したdata
にはString型の文字列が入っています。
dataDidSelectメソッドはString型の引数を受け取れる仕様にしているので、これで問題なく渡せます。
値を受け取った後の処理
ViewControllerに処理を書きます。
extension ViewController: ToPassDataProtocol {
func dataDidSelect(data: String) {
stringLabel.text = data //この行を追加
}
}
処理の内容は受け取る側で全て記述することになっており、これが処理の委譲、と言われている理由とも言えます。
渡す側のSecondViewController側では、String型のdataという変数を渡すけど、渡した変数をどう使うかは知らん
ということになる訳です。
これでプレビューの通り実装できました。
改めてなぜdelegateを使用するのか?
サンプルが簡素なものだったので分かりにくいかもしれませんが、Protocolを使ったdelegateで値を渡すことによって、ViewControllerとSecondViewControllerの結びつきを減らすことができているんです。
コードを見てみると、処理を委任する側のSecondViewController
にはViewControllerの記述はありません。
ですので、ある日突然ViewControllerファイルが消え去ってしまっても委任する側は影響が無くて済むようになります。
試しにViewControllerを無くしてみる
仮に、SecondViewController側で、ViewControllerを取得していた場合・・・
// よくあるViewControllerを取得する書き方
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateInitialViewController() as! ViewController
こんな感じになると思います。
ViewControllerを取得していますね。
では次にViewControllerを全文コメントアウトしてみます。
//import UIKit
//
//class ViewController: UIViewController {
//
// @IBOutlet weak var stringLabel: UILabel! {
// didSet {
// stringLabel.text = "まだ選択されていません"
// }
// }
//
// @IBAction func tappedButton(_ sender: UIButton) {
// let secondStoryboard = UIStoryboard(name: "Second", bundle: nil)
// let secondVC = secondStoryboard.instantiateInitialViewController() as! SecondViewController
// // SecondViewControllerで定義したdelegateにself(ViewController)を設定する
// secondVC.delegate = self
// performSegue(withIdentifier: "goSecond", sender: nil)
// }
//
//}
//
//extension ViewController: ToPassDataProtocol {
//
// func dataDidSelect(data: String) {
// stringLabel.text = data
// }
//
//}
これでViewControllerというクラスは存在しなくなりました。
ではViewControllerを取得した場合と、今回の実装したdelegateを使用した場合で見比べてみます。
ViewControllerを取得した場合 | delegateを使用した場合 |
---|---|
ちょっと見づらいかもしれませんが、ViewControllerを取得した場合だと、エラーとなってしまっています。
参照元のクラスが無くなってしまったので当然ですね。
対してdelegateを使用した場合ですが、特にエラーも無く問題ありません。
これは処理を委任する側からは、delegateを介してメソッドを実行する
ということしか指定していないからです。
delegateを使用して処理を委任するということは誰が
・どのような処理をするのか
ということを委任される側に丸投げすることができ、ファイル間の依存関係を薄くすることができるということになります。
Protocol凄い!
実際、VC間で値を受け渡すという処理を実装しているにも関わらずファイル消してもエラー出ないって驚きです。
これがProtocol指向ってやつなのか。
Protocol凄い!
delegate凄い!
Swift凄い!
ゴシゴシ(-дゞ≡ ゚Д゚)スッスゲ-!!!!
最後に
ここまで読んでいただきありがとうございました。
delegateを使用した簡単な実装と、何故delegateを使うのかということを書いてきました。如何だったでしょうか?
私はまだ学習中の身ですので間違った内容もあるかと思いますが、少しでも皆様の参考になれば幸いです。