アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

UIViewController の画面遷移などの振る舞いを protocol で実装する

いこーよの iOS アプリの開発を担当している namikata です。今回は UIViewController の画面遷移などの振る舞いを protocol で実装する方法を紹介したいと思います。色々な機能の実装を進めていく上で、こう書いたら使い回しきくしいいんじゃないかなぁ、と考えながら辿り着いた実装なので、まだ見えていない問題点があるかもしれませんが、自分の中では、開発効率が上がっていい感じで機能しています。

開発に携わっている いこーよアプリ をサンプルに書いて行きたいと思います。画面遷移などのアクションを protocol で書く事になった発端は、トップページ ( TopViewController ) のここの処理から始まりました。

f:id:t-namikata:20180921161029p:plain:w300

ユーザーさんが投稿してくれた口コミの一覧なんですが、CollectionView で実装していて、このセル( ReviewCell )では以下のアクションを行っています。

  1. 画像をタップすると遊び場施設の詳細画面に遷移する
  2. ユーザーアイコンをタップするとユーザー画面に遷移する
  3. 口コミのもっと見るをタップすると口コミ詳細画面 ( ReviewViewController ) に遷移する

まずは何も考えずに delegate で以下のような感じで実装を開始しました。

protocol ReviewCellDelegate : class {
    func transitionFacility(_ facility: Facility) // 遊び場施設の詳細画面に遷移する処理
    func transitionUser(_ user: User) // ユーザー画面に遷移する処理
    func transitionReview(_ review: Review) // 口コミ詳細画面に遷移する処理
}

class ReviewCell: UICollectionViewCell {
    private weak var delegate: ReviewCellDelegate?

    // 実際には facility, review, user などのデータを渡してあげます
    func setup(delegate: ReviewCellDelegate) {
        self.delegate = delegate
    }

    @IBAction func facilityClicked() {
        delegate?.transitionFacility(facility)
    }

    @IBAction func userClicked() {
        delegate?.transitionUser(user)
    }

    @IBAction func reviewClicked() {
        delegate?.transitionReview(review)
    }
}

class TopViewController: UIViewController, ReviewCellDelegate, UICollectionViewDataSource {
    func transitionFacility(_ facility: Facility) {
        // 遊び場施設の詳細画面に遷移
    }

    func transitionUser(_ user: User) {
        // ユーザー画面に遷移に遷移
    }

    func transitionReview(_ review: Review) {
        // 口コミ詳細画面に遷移
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ReviewCell
        cell.setup(delegate: self)
        return cell
    }
}

delegate パターンで実装しました。次に、口コミ本文のもっと見るをタップした時に遷移する、口コミ詳細画面 ( ReviewViewController ) の実装に移りました。口コミ詳細画面 はこんな風になっています。

f:id:t-namikata:20180921155814p:plain:w300

ここで口コミ詳細画面で行えるアクションを見てみると

  1. 施設名をタップするとスポット詳細画面に遷移する
  2. ユーザーアイコンをタップするとユーザー画面に遷移する
  3. 参考になった を押すと口コミに参考になったができるようになる。

といった内容になっていて、先ほどの ReviewCell と 1 〜 2 は同じアクションで 3 が追加された感じになっています。 ReviewViewController を愚直に実装すると以下のようになりました。

class ReviewViewController: UIViewController {
    @IBAction func facilityClicked() {
        // 遊び場施設の詳細画面に遷移
    }

    @IBAction func userClicked() {
        // ユーザー画面に遷移に遷移
    }

    @IBAction func likeClicked() {
        // 未実装
    }
}

TopViewController の transitionFacility と ReviewViewController の facilityClicked は同じように遊び場施設の詳細画面に遷移するアクションです。それぞれ画面遷移する処理を書いてあげればいいのですが、よくよく考えて見ると、

  • 口コミされた施設に遷移する
  • ユーザーをタップしたらユーザー画面に遷移する
  • 一覧の場合は、口コミ本文をタップしたら口コミ詳細が見れる
  • 口コミにいいねができる

といったアクションは、口コミに対するアクションとして、口コミを表示している他の画面でも使いそうに思えてきます。元々 ReviewCellDelegate で定義しているのは、口コミに対して行えるアクションになっているので、その振る舞いを protocol extension で実装してあげれば、色々使い回せるんじゃないかと思いました。

ReviewCellDelegate を ReviewActionable に名前を変更し、以下のように extension を書いてみます。

protocol ReviewActionable : class {
    func transitionFacility(facility: Facility) // 画像をタップした時の処理
    func transitionUser(user: User) // ユーザーをタップした時の処理
    func transitionReview(review: Review) // 口コミ本文のもっと見るをタップした時の処理
}

extension ReviewActionable where Self: UIViewController {
    func transitionFacility(_ facility: Facility) {
        // 遊び場施設の詳細画面に遷移する処理
    }

    func transitionUser(_ user: User) {
        // ユーザー画面に遷移に遷移する処理
    }

    func transitionReview(_ review: Review) {
        // 口コミ詳細画面に遷移する処理
    }
}

そして ReviewViewController で ReviewActionable を採用する事で、口コミに対する振る舞いを手に入れることができるようになりました。

extension ReviewViewController: ReviewActionable {}

class ReviewViewController: UIViewController {
    @IBAction func facilityClicked() {
        transitionFacility(facility)
    }

    @IBAction func userClicked() {
        transitionUser(user)
    }

    @IBAction func likeClicked() {
        // 未実装
    }
}

TopViewController からは ReviewCellDelegate ( 現 ReviewActionable ) に関する記述が消え、以下のようになりました。

extension TopViewController: ReviewActionable {}

class TopViewController: UIViewController, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ReviewCell
        cell.setup(delegate: self)
        return cell
    }
}

ただ TopViewController から、遷移する処理がバッサリ消えてしまったので、どこで何をやっているのか、コードの可読性が落ちるんじゃないかといった懸念があり、今、色々試験運用中です。 口コミ詳細画面では、口コミに対して「いいね」ができるので、その実装も protocol に追加して見ましょう。

protocol ReviewActionable : class {
    // ... 略 ...
    func likedReview(_ review: Review) // 口コミにいいねする
}

extension ReviewActionable where Self: UIViewController {
    // ... 略 ...
    func likedReview(_ review: Review) {
        // 口コミ登録アクション
    }
}
extension ReviewViewController: ReviewActionable {}

class ReviewViewController: UIViewController {
    // ... 略 ...
    @IBAction func likeClicked() {
        likedReview(review)
    }
}

こうする事で、今は必要ないですが、 TopViewController でも、口コミにいいねをしたいとなったら、既にいいねを行うアクションを protocol extension で定義してあるので、実装の追加も容易に行えます。開発中に、横スクロールだけでなく、縦スクロールで、もっと口コミが見れたらいいよね、って事で、機能が追加され、もっと見るボタンが設置されました。

f:id:t-namikata:20180921155925p:plain:w300

もっと見るを押した時の遷移先のページ ( ReviewListViewController ) はこのようになっています。

f:id:t-namikata:20180921155849p:plain:w300

TableView で実装することになっても、対応は簡単です。

extension ReviewListViewController: ReviewActionable {}

class ReviewListViewController: UIViewController, UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ReviewTableViewCell
        cell.setup(delegate: self)
        return cell
    }
}

class ReviewTableViewCell: UITableViewCell {
    private weak var delegate: ReviewActionable?

    func setup(delegate: ReviewActionable) {
        self.delegate = delegate
    }

    @IBAction func facilityClicked() {
        delegate?.transitionFacility(facility: facility)
    }

    // ... 略 ...
}

このようにして、複数画面で UIViewController の振る舞いに関する処理を protocol を利用してスムーズに開発する事が出来ました。最終的なコードは以下のようになりました。

// UIViewController が口コミに対して行うアクションをまとめた protocol
protocol ReviewActionable : class {
    func transitionFacility(_ facility: Facility) // 画像をタップした時の処理
    func transitionUser(_ user: User) // ユーザーをタップした時の処理
    func transitionReview(_ review: Review) // 口コミ本文のもっと見るをタップした時の処理
    func likedReview(_ review: Review) // 口コミにいいねする
}

extension ReviewActionable where Self: UIViewController {
    func transitionFacility(_ facility: Facility) {
        // 遊び場施設の詳細画面に遷移
    }

    func transitionUser(_ user: User) {
        // ユーザー画面に遷移に遷移
    }

    func transitionReview(_ review: Review) {
        // 口コミ詳細画面に遷移
    }

    func likedReview(_ review: Review) {
        // 口コミ登録アクション
    }
}
// トップ画面
extension TopViewController: ReviewActionable {}

class TopViewController: UIViewController, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ReviewCell
        cell.setup(delegate: self)
        return cell
    }
}

class ReviewCell: UICollectionViewCell {
    private weak var delegate: ReviewActionable?

    func setup(delegate: ReviewActionable) {
        self.delegate = delegate
    }

    @IBAction func facilityClicked() {
        delegate?.transitionFacility(facility)
    }

    @IBAction func userClicked() {
        delegate?.transitionUser(user)
    }

    @IBAction func reviewClicked() {
        delegate?.transitionReview(review)
    }
}
// 口コミ詳細画面
class ReviewViewController: UIViewController {
    @IBAction func facilityClicked() {
        transitionFacility(facility)
    }

    @IBAction func userClicked() {
        transitionUser(user)
    }

    @IBAction func reviewClicked() {
        transitionReview(review)
    }

    @IBAction func likeClicked() {
        likedReview(review)
    }
}
// 口コミ一覧画面
extension ReviewListViewController: ReviewActionable {}

class ReviewListViewController: UIViewController, UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ReviewTableViewCell
        // 本当は facility, review, user などの情報も渡す
        cell.setup(delegate: self)
        return cell
    }
}

class ReviewTableViewCell: UITableViewCell {
    private weak var delegate: ReviewActionable?

    func setup(delegate: ReviewActionable) {
        self.delegate = delegate
    }

    @IBAction func facilityClicked() {
        delegate?.transitionFacility(facility: facility)
    }

    @IBAction func userClicked() {
        delegate?.transitionUser(user)
    }

    @IBAction func reviewClicked() {
        delegate?.transitionReview(review)
    }
}

最後に

アクトインディでは、一緒に働いてくれるエンジニアを募集しております。 興味のある方はぜひお越しください!