iOSエンジニアの庄司(@WorldDownTown)です。 iQONのiOSアプリ内部で使われている画面遷移処理をOSSライブラリ化したのでご紹介します。
TL;DR
UINavigationController
での遷移時に、タップした画像をズームして遷移するトランジション処理をSwiftライブラリ化しました。- エッジスワイプでもズームアウトして戻ることができます。
ライブラリ化した経緯
Pinterestをはじめ、画像がズームインしながら画面遷移するアプリは今や珍しくありません。
この表現を実現するライブラリはいくつか存在しますが、通常のUINavigationController
のようにスワイプで戻れなくなったり、スワイプできても通常のスワイプとは違って指の動きに同期しないものが多い印象です。
iQONのアイテム詳細ページではこのジェスチャー周辺の実装がしっかりできているので、OSSとして公開したら需要があるかもと思い、ライブラリ化に踏み切りました。
特徴
エッジスワイプ
通常のUINavigationController
と同様に、エッジスワイプで前のViewControllerに戻る事ができます。
上記のアニメーションGIFを見ていただくとわかりやすいと思います。
Objective-Cプロジェクトでも使えます
作成したクラスはすべてFoundation
, UIKit
のクラスを継承しているため、Objective-Cのコードからも利用できます。
使い方
このライブラリを使って画面遷移のアニメーションを実装するには3つのステップがあります。
UINavigationControllerDelegate設定
画面遷移元のViewController設定
画面遷移先のViewController設定
1. UINavigationControllerDelegate 設定
UINavigationControllerDelegate
で画面遷移対象のViewControllerをチェックしてアニメーションをします。
ZoomNavigationControllerDelegate
オブジェクトをdelegate
に設定するだけです。
class NavigationController: UINavigationController { private let zoomNavigationControllerDelegate = ZoomNavigationControllerDelegate() required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) delegate = zoomNavigationControllerDelegate } }
2. 画面遷移元のViewController設定
アニメーション対象のUIImageView
を返したり、アニメーション中に元の画像を非表示にできるように画面遷移前後のZoomTransitionSourceDelegate
のメソッドを実装します。
extension ImageListViewController: ZoomTransitionSourceDelegate { // アニメーション対象のUIImageViewを返す func transitionSourceImageView() -> UIImageView { return selectedImageView } // スクリーンに対するアニメーション開始位置を返す func transitionSourceImageViewFrame(forward forward: Bool) -> CGRect { guard let selectedImageView = selectedImageView else { return CGRect.zero } return selectedImageView.convertRect(selectedImageView.bounds, toView: view) } // 画面遷移直前 func transitionSourceWillBegin() { selectedImageView?.hidden = true } // 画面遷移完了後 func transitionSourceDidEnd() { selectedImageView?.hidden = false } // 画面遷移キャンセル後 func transitionSourceDidCancel() { selectedImageView?.hidden = false } }
3. 画面遷移先のViewController設定
ZoomTransitionSourceDelegate
と同様の目的で画面遷移先のViewController向けの設定のため、ZoomTransitionDestinationDelegate
のメソッドを実装します。
extension ImageDetailViewController: ZoomTransitionDestinationDelegate { // 画面遷移完了後、及び、ポップ時のUIImageViewの配置 func transitionDestinationImageViewFrame(forward forward: Bool) -> CGRect { if forward { let x: CGFloat = 0.0 let y = topLayoutGuide.length let width = view.frame.width let height = width * 2.0 / 3.0 return CGRect(x: x, y: y, width: width, height: height) } else { return largeImageView.convertRect(largeImageView.bounds, toView: view) } } // 画面遷移直前 func transitionDestinationWillBegin() { largeImageView.hidden = true } // 画面遷移完了後 func transitionDestinationDidEnd(transitioningImageView imageView: UIImageView) { largeImageView.hidden = false largeImageView.image = imageView.image } // 画面遷移キャンセル後 func transitionDestinationDidCancel() { largeImageView.hidden = false } }
リポジトリにDemoプロジェクトがあるので、そちらもご覧ください。
ライブラリの内部実装
ズームアニメーションの仕組み
UIViewControllerAnimatedTransitioning
プロトコルを採用したZoomTransitioning
が画像がズームするアニメーション処理を実行しています。
UIViewControllerAnimatedTransitioning
による画面遷移アニメーションについては、下記のQiita記事が参考になったので、そちらをご覧ください。
スワイプで戻る
UIPercentDrivenInteractiveTransition
を継承し、UIGestureRecognizerDelegate
を採用したZoomInteractiveTransition
というクラスがスワイプによる画面遷移を実現させています。
let zoomInteractiveTransition = ZoomInteractiveTransition() let gesture = navigationController.interactivePopGestureRecognizer gesture?.delegate = zoomInteractiveTransition gesture?.addTarget(zoomInteractiveTransition, action: #selector(ZoomInteractiveTransition.handlePanGestureRecognizer(_:)))
UINavigationController
のinteractivePopGestureRecognizer
というプロパティはUIScreenEdgePanGestureRecognizer
クラスで、このGestureRecognizerが通常のエッジスワイプで戻る動作を可能にしています。
ZoomInteractiveTransitioning
でinteractivePopGestureRecognizer
のジェスチャー処理を受け取ることで、ZoomTransitioning
が実行するアニメーションを使ってエッジスワイプで戻れるようになります。
// UINavigationControllerDelegate func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return zoomInteractiveTransition.interactive ? zoomInteractiveTransition : nil }
エッジスワイプのジェスチャーを受け取ってナビゲーションを戻るときだけzoomInteractiveTransition
を返して、指の動きを反映したインタラクティブな画面遷移をします。
class ZoomInteractiveTransition: UIPercentDrivenInteractiveTransition { var interactive = false // スワイプで戻るフラグ。ジェスチャーを受け取ったらtrueにする @objc func handlePanGestureRecognizer(recognizer: UIScreenEdgePanGestureRecognizer) { let view = recognizer.view! let progress = recognizer.translationInView(view).x / view.bounds.width switch recognizer.state { case .Changed: updateInteractiveTransition(progress) case .Cancelled, .Ended: if progress > 0.33 { finishInteractiveTransition() } else { cancelInteractiveTransition() } default: break } }
UINavigationController
のinteractivePopGestureRecognizer
のジェスチャーで呼ばれるメソッドの方では、スワイプする指の位置ごとに、UIPercentDrivenInteractiveTransition
のメソッドを呼び出してアニメーションの進捗を反映させます。そうすると、ZoomTransitioning
のアニメーションが指の位置に合わせて動作します。
複雑な処理に見えますが、もしUIPercentDrivenInteractiveTransition
を使わなかった場合、スワイプで戻る時もZoomTransitionig
と同じアニメーション処理を別途実装しないといけません。
さいごに
UINavigationController
での遷移時に、タップした画像をズームインしながらアニメーションするZoomTransitioning
を紹介しました。
このライブラリは、iQONでの仕様を基に最低限の機能で公開しています。
バグ報告や改善案など (OSS化の真の目的はこれだったり…)、Pull Request お待ちしております。