炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

Swift2 - クラスの継承とプロトコルの適合(採用)の両方の条件を満たす条件をジェネリクスで書く


Swift2.2, Xcode7

modal で呼び出す画面は、どの ViewController からも呼び出される可能性がある為、ページ遷移用の Utility に定義する様にしています。その時に困るのが、 UIViewController を継承して、かつ、モーダルを閉じる delegate ( protocol )を採用しているケースです。例として以下のようなケースを考えたいと思います。

  • modal を呼び出す ParentViewController 。 delegate で modal を閉じる。
  • modal で呼び出される側は ChildViewController
  • storyboard は ViewController と 1 対 1 で分割している

  • ParentViewController

protocol ChildViewControllerDelegate: class {
    func modalDismiss()
}

class ParentViewController: UIViewController, ChildViewControllerDelegate {
    ...

    func modalChild() {
        // TransitionUtility は ChildViewController を modal で呼び出す処理
        TransitionUtility.modalChild(self)
    }

    func modalDismiss() {
        self.dismissViewControllerAnimated(true, completion: nil)
    }
  • ModalViewController
class ModalViewController: UIViewController {
    weak var delegate: ChildViewControllerDelegate?

    ...

    func close() {
        self.delegate.modalDismiss()
    }
}

このような状況で modal を表示する処理を書こうと思うと UIViewController の継承と EditEmailViewControllerDelegate の Protocol の採用でちょっと困ったことになります。

struct TransitionUtility {
  static func modal(viewController: UIViewController) {
        let storyboard = UIStoryboard(name: "Child", bundle: nil)
        guard let modalViewController = storyboard.instantiateViewControllerWithIdentifier("Child") as? ChildViewController else {
            return
        }
        modalViewController.delegate = viewController
        // ↑ viewController は ChildViewControllerDelegate を採用していないので delegate に指定できないよと怒られる。
        viewController.presentViewController(modalViewController, animated: true, completion: nil)      
  }
}
struct TransitionUtility {
  static func modal(viewController: EditEmailViewControllerDelegate) {
        let storyboard = UIStoryboard(name: "Child", bundle: nil)
        guard let modalViewController = storyboard.instantiateViewControllerWithIdentifier("Child") as? ChildViewController else {
            return
        }
        modalViewController.delegate = viewController
        viewController.presentViewController(modalViewController, animated: true, completion: nil)
        // ↑ viewController は EditEmailViewControllerDelegate なので UIViewController の持つ presentViewController は利用できない。
  }
}

このような時に利用できるのがジェネリクスです。ジェネリクスを利用することで UIViewController を継承しており、かつ、ChildViewControllerDelegate を採用しているクラスを引数に定義することができます。実装方法は以下です。

static func modalChild<T where T: UIViewController, T: ChildViewControllerDelegate>(viewController: T) {
    let storyboard = UIStoryboard(name: "Child", bundle: nil)
    guard let modalViewController = storyboard.instantiateViewControllerWithIdentifier("Child") as? ChildViewController else {
        return
    }
    modalViewController.delegate = viewController
    viewController.presentViewController(modalViewController, animated: true, completion: nil)
}

これで ChildViewController の modal の呼び出しは TransitionUtility.modalChild(self) といった形で呼び出せるようになります。