SwiftでTumblrのリアクションみたいにモーダルウィンドウがにゅっと出るやつを作る
この記事は ピクシブ株式会社 Advent Calendar 2015 16日目の記事です。
昨日は、id:catatsuyの ピクシブ社内広告サーバーに新機能を追加するためにボクがやったこと - pixiv inside でした!
どうもどうも、エンジニアのid:FromAtomです。pixiv SketchというサービスのiOSアプリデベロッパーをしております。 いろんなイベントで話をするたびに「本当に新卒ですか?」「3年目ぐらいの貫禄がある。」と言われますが新卒です。よろしくお願いいたします。
さて、今日は『SwiftでTumblrのリアクションみたいにモーダルウィンドウがにゅっと出るやつを作る』方法を紹介します。 文字で読むと「なんのこっちゃ?」という感じだと思うので、まずはこちらを御覧ください。
TumblrのiOSアプリでは、このようにリアクションをタップするとモーダルがにゅっと出てきて、リアクションの一覧を確認することができます。ちなみに、このかわいいネコは我が家の愛猫しじみです。Tumblrでしじみ生活という猫記録を公開していますので、ぜひフォローして激かわな写真・動画をお楽しみ下さい。
猫の話はさておき、にゅっと出るモーダルの作り方についてです。
サンプル
GitHubからcloneして試したい方はこちら。
環境
- Swift2.1
- Xcode Version 7.2
- iOS8以降
モーダルを作る
まずはじめに、UIButtonをTapしたらモーダルが表示されるようにしましょう。Xcodeから新規プロジェクトを作成します。テンプレートはSingle View Application
を選択しましょう。
これからいじっていくファイルは次の4つです。
- Main.storyboard → 元からある
- ViewController.swift → 元からある
- ModalViewController.swift → 新しく作る
- CustomPresentationController.swift → 新しく作る
Main.storyboard
Main.storyboard
にはUIButton
を設置しておきます。
ViewController.swift
ViewController.swift
はこのように記載します。@IBAction func OpenButtonTouchUpInside(sender: UIButton)
には先程Main.storyboard
で設置したUIButtonを紐付けましょう。
class ViewController: UIViewController { var atButton: UIButton? override func viewDidLoad() { super.viewDidLoad() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } @IBAction func OpenButtonTouchUpInside(sender: UIButton) { atButton = sender let modalViewController = ModalViewController() modalViewController.modalPresentationStyle = .Custom modalViewController.transitioningDelegate = self presentViewController(modalViewController, animated: true, completion: nil) } } extension ViewController: UIViewControllerTransitioningDelegate { func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? { return CustomPresentationController(presentedViewController: presented, presentingViewController: presenting) } }
ModalViewController.swift
モーダルとして表示される画面です。この中で特殊なコードを書く必要はありません。わかりやすくなるように、背景色を緑色にする処理だけ書いておきましょう。
class ModalViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor.greenColor() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }
CustomPresentationController.swift
ViewController.swift
で実装したpresentationControllerForPresentedViewController:
から呼ばれます。
final class CustomPresentationController: UIPresentationController { var overlayView = UIView() // 表示トランジション開始前に呼ばれる override func presentationTransitionWillBegin() { guard let containerView = containerView else { return } overlayView.frame = containerView.bounds overlayView.gestureRecognizers = [UITapGestureRecognizer(target: self, action: "overlayViewDidTouch:")] overlayView.backgroundColor = UIColor.blackColor() overlayView.alpha = 0.0 containerView.insertSubview(overlayView, atIndex: 0) presentedViewController.transitionCoordinator()?.animateAlongsideTransition({ [weak self] context in self?.overlayView.alpha = 0.7 }, completion: nil) } // 非表示トランジション開始前に呼ばれる override func dismissalTransitionWillBegin() { presentedViewController.transitionCoordinator()?.animateAlongsideTransition({ [weak self] context in self?.overlayView.alpha = 0.0 }, completion: nil) } // 非表示トランジション開始後に呼ばれる override func dismissalTransitionDidEnd(completed: Bool) { if completed { overlayView.removeFromSuperview() } } let margin = (x: CGFloat(30), y: CGFloat(220.0)) override func sizeForChildContentContainer(container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize { return CGSize(width: parentSize.width - margin.x, height: parentSize.height - margin.y) } override func frameOfPresentedViewInContainerView() -> CGRect { var presentedViewFrame = CGRectZero let containerBounds = containerView!.bounds let childContentSize = sizeForChildContentContainer(presentedViewController, withParentContainerSize: containerBounds.size) presentedViewFrame.size = childContentSize presentedViewFrame.origin.x = margin.x / 2.0 presentedViewFrame.origin.y = margin.y / 2.0 return presentedViewFrame } // レイアウト開始前に呼ばれる override func containerViewWillLayoutSubviews() { overlayView.frame = containerView!.bounds presentedView()!.frame = frameOfPresentedViewInContainerView() } // レイアウト開始後に呼ばれる override func containerViewDidLayoutSubviews() { } // overlayViewをタップしたときに呼ばれる func overlayViewDidTouch(sender: AnyObject) { presentedViewController.dismissViewControllerAnimated(true, completion: nil) } }
sizeForChildContentContainer:
ではModalViewController
がどのくらいのサイズで表示されるかを決めています。そしてそれを元にframeOfPresentedViewInContainerView:
で表示位置の調整をしています。ここの処理で、上下左右にマージンをとったModalが画面中央に表示されるようになります。
下からにゅっと出る様子
ここまでの実装で、モーダルが下からにゅっと出るようになりました。このトランジション自体は標準的なものですね。
ボタンからにゅっと出るようにする
ここまでの実装でモーダルとしての見た目が整いました。しかし、このままではボタンからではなく画面下から出てきてしまいます。それでは、ボタンからにゅっと出すためにUIViewControllerAnimatedTransitioning
を使っていきましょう。
いじるファイルは以下の2つです。
- ViewController.swift → 元からある
- CustomAnimatedTransitioning.swift → 新しく作る
ViewController.swift
次のextension
を追記します。
extension ViewController { func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CustomAnimatedTransitioning(isPresent: true, atButton: atButton) } func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CustomAnimatedTransitioning(isPresent: false, atButton: atButton) } }
CustomAnimatedTransitioning.swift
ViewController.swift
で呼ばれているCustomAnimatedTransitioning(isPresent: Bool, atButton: UIButton)
を実装します。
final class CustomAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning { let isPresent: Bool let atButton: UIButton init(isPresent: Bool, atButton: UIButton?) { self.isPresent = isPresent self.atButton = atButton ?? UIButton(frame: CGRectZero) } func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { return 0.5 } func animateTransition(transitionContext: UIViewControllerContextTransitioning) { if isPresent { animatePresentTransition(transitionContext) } else { animateDissmissalTransition(transitionContext) } } // 表示する時のアニメーション func animatePresentTransition(transitionContext: UIViewControllerContextTransitioning) { guard let presentingController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey), let presentedController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey), let containerView = transitionContext.containerView() else { return } presentedController.view.layer.cornerRadius = 4.0 presentedController.view.clipsToBounds = true presentedController.view.alpha = 0.0 presentedController.view.transform = CGAffineTransformMakeScale(0.01, 0.01) containerView.insertSubview(presentedController.view, belowSubview: presentingController.view) UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.6, options: .CurveLinear, animations: { presentedController.view.alpha = 1.0 presentedController.view.frame.origin.x = containerView.bounds.size.width - self.atButton.frame.origin.x presentedController.view.frame.origin.y = containerView.bounds.size.height - self.atButton.frame.origin.y presentedController.view.transform = CGAffineTransformIdentity }, completion: { finished in transitionContext.completeTransition(true) }) } // 非表示する時のアニメーション func animateDissmissalTransition(transitionContext: UIViewControllerContextTransitioning) { guard let presentedController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else { return } UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.6, options: .CurveLinear, animations:{ presentedController.view.alpha = 0.0 presentedController.view.transform = CGAffineTransformMakeScale(0.01, 0.01) presentedController.view.frame.origin.x = self.atButton.frame.origin.x presentedController.view.frame.origin.y = self.atButton.frame.origin.y }, completion: { finished in transitionContext.completeTransition(true) }) } }
ボタンからにゅっと出る様子
ちゃんとUIButtonからにゅっと出てるか確認するために、いくつかUIButtonを追加して、@IBAction func OpenButtonTouchUpInside(sender: UIButton)
にひもづけましょう。
これでボタンからモーダルがにゅっと出るようになりました!やった!
参考文献
クラスメソッドさんの記事が大変参考になりました。
同じ手法でこんな画面遷移を実装することも可能です。
まとめ
UIPresentationControllerを使うと、自分でカスタムしたモーダルウィンドウを作ることができます。 また、UIViewControllerAnimatedTransitioningを使うと、モーダルを表示する際のアニメーションを設定することができることがわかりました。
今回は、これらを組み合わせてTumblrのリアクションの様なにゅっと出るモーダルを作成しましたが、 実装を変えることでTwitter公式アプリの投稿画面で使われているトランジションも実現できます。
また、今回はUIButtonから緑のUiViewが出てきましたが、サムネイル画像をタップするとにゅっと画像を詳細表示をする遷移も同じ仕組みで実装可能です。 他にも、色々とおしゃれなことができそうですね。
ちなみにピクシブ株式会社では、にゅっとしてしゅっとしたアプリを一緒に作ってくれるエンジニア・アルバイトを募集中です。こちらからしゅっとエントリーできます。
明日は同じく新卒の @RinKeiHotmanが担当です!お楽しみに!