LoginSignup
14
17

More than 3 years have passed since last update.

[Swift] Protocolを使ったdelegateで値の受け渡しをする

Last updated at Posted at 2021-03-19

はじめに

SwiftによるViewController間での値の受け渡しには、Protocolによるdelegateを使用して行うことが良いとされています。

学習の振り返りも兼ねて、実装手順やその思考を残しておくことにしました。

なぜdelegateを使用するのか?

様々な理由があるとは思いますが、私は次の点が重要と考えています。

  • 処理を他に委任することで、ファイル間の依存関係を減らすことができる

これは、ViewControllerなどのClass間の結び付きを少なくした方が良いよね、という思想からきているようです。
どういうことなのかは実装しながら見ていきましょう。

概要

  • TableViewに表示されている文字列を、別のViewControllerに渡して表示する
  • 画面遷移はNavigationControllerを使用する
こんな感じ(プレビュー)

環境

Xcode 12.4

実装

Viewの準備

ViewComtrollerを跨いで値を渡したいので、2画面を作ります。

  • LabelとButtonを持つView(ViewController)
  • TableViewを持つView(SecondViewController)
  • NavigationControllerを設置

1aスクリーンショット.png

LabelとTableViewはIBOutlet、ButtonはIBAcitonでそれぞれ接続しておきましょう。

画面遷移を実装する

画面遷移はNavigationControllerを使用し、コードで実装します。

ではさっそくViewControllerファイルに、Secondへ画面遷移するためのコードを書いていきます。

ViewController.swift
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に値を持たせる

SecondViewController.swift
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を出力するようにしています。

左:遷移前 / 右:遷移後
3aスクリーンショット.png

値の受け渡しのためのProtocolを定義する

いよいよProtocolを書いていきます。
今回はTableViewCellが持つString型の文字列を渡すための記述をしていきます。

まずProtocolを書くswiftファイルを作成します。ファイル名はToPassDataProtocolとします。
(プロトコルってどういうファイル名にするのが適切なんだろうか・・・?)

ToPassDataProtocol.swift
import Foundation

protocol ToPassDataProtocol: class {
    func dataDidSelect(data: String)
}

このプロトコルを準拠させたクラスに、String型のdataを引数として受け取るdataDidSelectというメソッドの処理を記述することになります。

delegateを定義

SecondViewController.swift
// 一部抜粋
class SecondViewController: UIViewController {  
    let dataStrings = ["First", "Second", "Third", "Another", "More"]
    // ↓SecondViewController内にこの行を追加
    weak var delegate: ToPassDataProtocol? // 処理を任せる相手を保持する
}

delegateを定義します。
セルをタップした時に値を渡すという処理を実行したいので、tableView(didSelectRowAt)を持つSecondViewControllerファイルに記述しています。

受取側をProtocolに準拠させる

Protocolで値を受け取るために準拠させます。

ViewController.swift
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)の処理を書き換えます。

SecondViewController.swift
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に処理を書きます。

ViewController.swift
extension ViewController: ToPassDataProtocol {    
    func dataDidSelect(data: String) {
        stringLabel.text = data //この行を追加
    }
}

処理の内容は受け取る側で全て記述することになっており、これが処理の委譲、と言われている理由とも言えます。
渡す側のSecondViewController側では、String型のdataという変数を渡すけど、渡した変数をどう使うかは知らんということになる訳です。

これでプレビューの通り実装できました。

改めてなぜdelegateを使用するのか?

サンプルが簡素なものだったので分かりにくいかもしれませんが、Protocolを使ったdelegateで値を渡すことによって、ViewControllerとSecondViewControllerの結びつきを減らすことができているんです。

コードを見てみると、処理を委任する側のSecondViewControllerにはViewControllerの記述はありません。
ですので、ある日突然ViewControllerファイルが消え去ってしまっても委任する側は影響が無くて済むようになります。

試しにViewControllerを無くしてみる

仮に、SecondViewController側で、ViewControllerを取得していた場合・・・

SecondViewController.swift
// よくあるViewControllerを取得する書き方
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateInitialViewController() as! ViewController

こんな感じになると思います。
ViewControllerを取得していますね。

では次にViewControllerを全文コメントアウトしてみます。

ViewController.swift
//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を使用した場合
6aスクリーンショット.png 7aスクリーンショット.png

ちょっと見づらいかもしれませんが、ViewControllerを取得した場合だと、エラーとなってしまっています。
参照元のクラスが無くなってしまったので当然ですね。

対してdelegateを使用した場合ですが、特にエラーも無く問題ありません。
これは処理を委任する側からは、delegateを介してメソッドを実行するということしか指定していないからです。
delegateを使用して処理を委任するということは誰がどのような処理をするのかということを委任される側に丸投げすることができ、ファイル間の依存関係を薄くすることができるということになります。

Protocol凄い!

実際、VC間で値を受け渡すという処理を実装しているにも関わらずファイル消してもエラー出ないって驚きです。
これがProtocol指向ってやつなのか。

Protocol凄い!
delegate凄い!
Swift凄い!
ゴシゴシ(-дゞ≡ ゚Д゚)スッスゲ-!!!!

最後に

ここまで読んでいただきありがとうございました。
delegateを使用した簡単な実装と、何故delegateを使うのかということを書いてきました。如何だったでしょうか?

私はまだ学習中の身ですので間違った内容もあるかと思いますが、少しでも皆様の参考になれば幸いです。

14
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
17