Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What is going on with this article?
@jnosan_

protocolを使ったdelegateについて

protocolとは、「手続き・手順」といった意味だが、
"こういった実装をしてくださいね"といった「約束事・決まり事」と解釈した方が、
iOS開発においては理解しやすい。

protocol FooProtocol {

    var id: Int { get }

    func test1()

    func test2()

    func test3()

}

protocol FooProtocol2 {

    var name: String { get }

    func test4()

}

特徴

  • 中身のないクラスのようなもの
  • クラスなどに準拠されることが前提で作られる
  • クラスの継承より柔軟である

使用時

class BarClass: FooProtocol {

    var id: Int = 0

    func test1() {
        print("BarClassのtest1")
    }

    func test2() {
        print("BarClassのtest2")
    }

    func test3() {
        print("BarClassのtest3")
    }   

}
  • BarClassはFooProtocolに 準拠 している
  • BarClassでは、FooProtocolに用意されているメソッドの中身を実装しなければエラーとなる
  • StructやEnumにも準拠できる
class BazClass: FooProtocol, FooProtocol2 {

    var id: Int = 0

    var name: String = "Taro"

    func test1() {
        print("BazClassのtest1")
    }

    func test2() {
        print("BazClassのtest2")
    }

    func test3() {
        print("BazClassのtest3")
    }   

    func test4() {
        print("BazClassのtest4")
    }

}
  • 同じプロトコルに準拠したクラスは、必ず同じメソッドが実行できる
  • protocolは複数準拠可能である

下記のように、プロトコルにプロトコルを準拠させることも可能。

protocol FooProtocol3: FooProtocol, FooProtocol2 {
    func test5()
}

class BarClass2: FooProtocol3 {

    var id: Int = 0
    var name: String = "Jiro"

    func test1() {
        print("BarClass2のtest1")
    }

    func test2() {
        print("BarClass2のtest2")
    }

    func test3() {
        print("BarClass2のtest3")
    }

    func test4() {
        print("BarClass2のtest4")
    }

    func test5() {
        print("BarClass2のtest5")
    }

}

上記の場合、FooProtocol3は、FooProtocol, FooProtocol2に準拠している。
したがってBarClass2では、FooProtocol, FooProtocol2のメソッドや変数を実装する必要がある。

また、extensionとして本来は実態のないprotocolの関数などにデフォルト実装を定義できる。

protocol FooProtocol4 {
    var numbaer: Int { get }

    func test6()
    func test7()

}

extension FooProtocol4 {
    func test6() {
        print("\(String(describing: type(of: self))) \(#function)")
    }
}

class BarClass3 {

}

extension BarClass3: FooProtocol4 {
    var numbaer: Int { 1 }

    //test6はデフォルトで定義されているので書かなくてもいい
    func test7() {
        print("\(String(describing: type(of: self))) \(#function)")
    }
}

let bar3 = BarClass3()
bar3.test6()
bar3.test7()

ここまで来て、クラスの継承ではだめなのかという疑問が生じる。

クラスの場合は、必ず継承元に実装があるため、継承元を辿っていかなければならない。

プロトコルはそれを解決する。

protocol FooProtocol {
    func test()
}
protocol FooProtocol2: FooProtocol {
    func test2()
}

protocol FooProtocol3 {
    func test3()
}
protocol FooProtocol4: FooProtocol2, FooProtocol3 {
    func test4()
}
class Foo: FooProtocol4 {
    func test() {/* 処理 */ }
    func test2() {/* 処理 */ }
    func test3() {/* 処理 */ }
    func test4() {/* 処理 */ }
}

Swiftは"Protocol指向"言語。
オブジェクト指向的な書き方ももちろん可能だが、
なるべくprotocolを使用することが推奨されている。

Swift言語自体がほとんどprotocolを用いて作られている。
iOSのUIKitなどを見れば、protocolを利用したdelegateの実装を見ることができる。

protocolを使ったdelegateとは

あるクラスの中にある処理を、他のクラスに任せること

  • iOS SDK標準のdelegateが多くある。
  • 自分のプログラムでdelegateの中身を実装して使用する。
  • 開発者は、delegateで「任される側」の処理を書く。

よく使われるtableViewの例

import UIKit

class TableViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView! {
        didSet {
            //二つのプロトコルに自分自身を代入
            tableView.delegate = self
            tableView.dataSource = self
        }
    }
}

extension TableViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("select cell \(indexPath)")
    }
}

extension TableViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = indexPath.row.description
        return cell
    }
}

delegateが解決する問題

delegateでは無い時

class NoDelegateViewController1: UIViewController {

    @IBAction func touchButton(_ sender: Any){

        guard let vc = UIStoryboard.init(name: "NoDelegate2", bundle: nil).instantiateInitialViewController() else {
            return
        }
        self.navigationController?.pushViewController(vc, animated: true)
    }
}

別のファイル

class NoDelegateViewController2: UIViewController {

    @IBAction func touchButton(_ sender: Any) {
        guard let vc = UIStoryboard.init(name: "NoDelegate3", bundle: nil).instantiateInitialViewController() else {
            return
        }
        self.navigationController?.pushViewController(vc, animated: true)
    }
}

上記のように、はじめのNoDelegateViewController1をビルドするためには、
NoDelegateViewController3が必要で、(UIのファイルも含めて)多くのファイルを要することになる。

これは密結合と呼ばれる状態である。

delegateの場合
(DelegateViewController2も同様なので省略)

//DelegateViewController1Delegateがもつ関数の実装はこのファイルにはない
protocol DelegateViewController1Delegate: AnyObject {
    func touchButtonDelegate1(nowVC: DelegateViewController1)
}

class DelegateViewController1: UIViewController {

    weak var delegate: DelegateViewController1Delegate? // 上のprotocolを準拠させたプロパティ

    @IBAction func touchButton(_ sender: Any) {
        // delegateの実装を持ってるファイルに処理を任せる(委譲)
        // つまり、"delegateパラメータのtouchButtonDelegate1を呼べ"
        delegate?.touchButtonDelegate1(nowVC: self)
    }
}

この protocol DelegateViewController1Delegate の中の、

func touchButtonDelegate1(nowVC: DelegateViewController1)

の実装自体は別のファイルにある。

下記は画面遷移を担当するクラスの例。


class Coordinator: DelegateViewController1Delegate, DelegateViewController2Delegate {

    //シングルトンパターンの書き方(あまりよくない書き方)
    static let shared = Coordinator()
    private init(){}

    // 最初の画面のviewcontorollerを呼ぶメソッド例
    func startVC(nowVC: UIViewController) {
        guard
            let navigationController = nowVC.navigationController,
            let vc = UIStoryboard.init(name: "Delegate1", bundle: nil).instantiateInitialViewController() as? DelegateViewController1 else {
                return
        }

        vc.delegate = self // ここで、先ほどのDelegateViewController1のdelegateパラメータの持ち主を私(Coodinator)に指定
        navigationController.pushViewController(vc, animated: true)

    }

    // 先ほどの、DelegateViewController1Delegateがもつfunctionの実装はこのファイルにある
    func touchButtonDelegate1(nowVC: Delegate1ViewController) {
        guard
            let navigationController = nowVC.navigationController,
            let vc = UIStoryboard.init(name: "Delegate2", bundle: nil).instantiateInitialViewController()  as? Delegate2ViewController else {
            return
        }
        vc.delegate = self
        navigationController.pushViewController(vc, animated: true)
    }

    // Delegate2ViewControllerDelegateがもつfunctionの実装はこのファイルにある
    func touchButtonDelegate2(nowVC: DelegateViewController2) {
        guard
            let navigationController = nowVC.navigationController,
            let vc = UIStoryboard.init(name: "Delegate3", bundle: nil).instantiateInitialViewController() as? DelegateViewController3 else {
                return
        }
        navigationController.pushViewController(vc, animated: true)
    }
}

delegateを使わない場合は、数珠つなぎ状態であるが、
delegateを使った場合は、上記ではCoordinatorクラスが司る。

したがって、VCそれぞれはお互いに依存していない。

ファイルがより増えた場合、delegateを使わない場合は密結合の度合いが大きくなってしまう。

iOS標準のSDKも、delegateの仕組みで提供されているからこそ、
利用する側が柔軟に処理を記述できると言える。

2
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
2
Help us understand the problem. What is going on with this article?