your3i’s blog

iOSエンジニア。頑張る⚉

iOS Custom Presentation & Transition (2) 〜UIPresentationControllerでカスタムモーダルを作る〜

UIPresentationControllerの役割

presentation controllerオブジェクトは、presented view controllerを管理する役割を持ってる。そして、presented view controller表示されてるときのスタイルを指定するなど。

ドキュメントに書いたやること

Set the size of the presented view controller.

Add custom views to change the visual appearance of the presented content.

Supply transition animations for any of its custom views.

Adapt the visual appearance of the presentation when changes occur in the app’s environment.

翻訳すると
  • presented view controllerのサイズを設定
  • 表示するときほかにカスタムビューが必要だったら追加
  • transitionが行われるときに、そのカスタムビューに必要なアニメーションを追加
  • アプリの環境が変わるとき、その変化に応じてpresentationを調整
これでわかること

前回で書いた通り、今回は半モーダルでdetailsViewControllerを出すのを作ってみる。
前回のリンク:
iOS Custom Presentation & Transition (1) 〜コード一行もない編〜 - your3i’s blog

それを作るとき:

  • detailsViewControllerの表示するサイズはpresentation controllerが決める
  • detailsViewControllerの後ろのdimming viewはpresentation controllerが追加する
  • 遷移するときのそのdimming viewのアニメーションもpresentation controllerが追加する
  • dimming viewのタップイベントもpresentation controllerが追加する
  • 端末の回転とか起きたとき、dimming viewとpresented viewのサイズなどはpresentation controllerが調整

(自分は、presentation controllerを使わず、presented view controllerで全部やってて、変な実装をしたことがある😂)

カスタムUIPresentationController

カスタムUIPresentationControllerの適用

まず、空でもいいから、UIPresentationControllerを継承したCustomPresentationControllerを作る。

import UIKit

class CustomPresentationController: UIPresentationController {

    override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
    }
}

そして、カスタムUIPresentationController使われるように、detailsViewControllerのmodalPresentationStyleを.customに設定し、CustomPresentationControllerオブジェクトをdetailsViewControllerに渡すUIViewControllerTransitioningDelegateのオブジェクトも設定する。

今回はdetailsViewController自分でUIViewControllerTransitioningDelegateにするようにする。

class DetailsViewController: UIViewController, UIViewControllerTransitioningDelegate {

    ...

    static func viewController() -> DetailsViewController {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let viewController = storyboard.instantiateViewController(withIdentifier: "DetailsViewController") as! DetailsViewController
        viewController.modalPresentationStyle = .custom
        viewController.transitioningDelegate = viewController
        return viewController
    }

    ...

    // MARK: - UIViewControllerTransitioningDelegate

    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return CustomPresentationController(presentedViewController: presented, presenting: presenting)
    }
}

効果をみてみよう:
今はただ空で継承したから、挙動の変化はまだない。
f:id:your3i:20181005132623g:plain

半画面にする

CustomPresentationControllerのframeOfPresentedViewInContainerViewをoverride。
containerViewは名前通り、presentationはこのビューの中で行われる。

override var frameOfPresentedViewInContainerView: CGRect {
    guard let containerView = containerView else {
        return .zero
    }

    let rect = CGRect(x: 0, y: containerView.bounds.height / 2.0, width: containerView.bounds.width, height: containerView.bounds.height / 2.0)
    return rect
}

効果をみてみよう:
f:id:your3i:20181005132427g:plain

DimmingViewを追加

このステップで、画面の残りの部分をoverlayするdimming viewを追加する。

まずdimming viewを作る。

class CustomPresentationController: UIPresentationController {

    private var dimmingView: UIView!

    override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        setupDimmingView()
    }

    private func setupDimmingView() {
        let view = UIView()
        view.backgroundColor = UIColor.black.withAlphaComponent(0.4)   //透明度を指定
        dimmingView = view
    }
    
    ...
}

presentation transition開始するときにdimming viewをsubviewとして追加。

    override func presentationTransitionWillBegin() {
        guard let containerView = containerView else {
            return
        }

        dimmingView.frame = containerView.bounds
        containerView.addSubview(dimmingView)
    }

    override func presentationTransitionDidEnd(_ completed: Bool) {
        // もしpresentation transition失敗した場合、detailsViewControllerを出せなかったから、dimming viewもremoveする
        if !completed {
            dimmingView.removeFromSuperview()
        }
    }

detailsViewControllerがdismissされたら、dismiss transitionが行われる。dismiss transition成功に終わったら、dimming viewもremoveする

    override func dismissalTransitionDidEnd(_ completed: Bool) {
        if completed {
            dimmingView.removeFromSuperview()
        }
    }

効果をみてみよう:
f:id:your3i:20181005132750g:plain

DimmingViewにアニメーションをつける

前のステップで、dimmingViewが追加れたかけど、出し方がちょっと微妙だね。このステップで、DimmingViewにアニメーションをつける。

表示するとき:
presentationTransitionWillBeginにアニメーションコードの追加。

    override func presentationTransitionWillBegin() {
        guard let containerView = containerView else {
            return
        }

        dimmingView.frame = containerView.bounds
        containerView.addSubview(dimmingView)

        // アニメーション→表示
        dimmingView.alpha = 0.0
        if let coordinator = presentedViewController.transitionCoordinator {
            coordinator.animate(
                alongsideTransition: { [weak self] _ in
                    self?.dimmingView.alpha = 1.0
                }, completion: nil)
        } else {
            dimmingView.alpha = 1.0
        }
    }

非表示するとき:
dismissalTransitionWillBeginを新しくoverrideして、そこに非表示のアニメーションを追加。

    override func dismissalTransitionWillBegin() {
        // アニメーション → 非表示
        if let coordinator = presentedViewController.transitionCoordinator {
            coordinator.animate(
                alongsideTransition: { [weak self] _ in
                    self?.dimmingView.alpha = 0.0
                }, completion: nil)
        } else {
            dimmingView.alpha = 0.0
        }
    }

効果をみてみよう:
f:id:your3i:20181005132839g:plain

タップしてdetailsViewControllerを閉じる

これは!もう簡単〜dimming viewにtap gesture recognizerを追加すればいい。

    private func setupDimmingView() {
        let view = UIView()
        view.backgroundColor = UIColor.black.withAlphaComponent(0.4)
        dimmingView = view

        // tap gesture recognizer を追加
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        dimmingView.addGestureRecognizer(tapGesture)
    }

    @objc func handleTap(_ recognizer: UITapGestureRecognizer) {
        presentedViewController.dismiss(animated: true, completion: nil)
    }
完成!!!

f:id:your3i:20181005132917g:plain