画像がズームインしながら画面遷移するSwiftライブラリを公開しました



> この記事は[VASILY DEVELOPERS BLOG](http://tech.vasily.jp/entry/zoom_transitioning)にも同じ内容で投稿しています。よろしければ他の記事もご覧ください。


iQONのiOSアプリ内部で使われている画面遷移処理をOSSライブラリ化したのでご紹介します。

# TL;DR
- `UINavigationController`での遷移時に、タップした画像をズームして遷移するトランジション処理をSwiftライブラリ化しました。
- エッジスワイプでもズームアウトして戻ることができます。

https://github.com/WorldDownTown/ZoomTransitioning

<img src="https://github.com/WorldDownTown/ZoomTransitioning/raw/master/images/demo.gif" width="250px" height="445px" />

# ライブラリ化した経緯
Pinterestをはじめ、画像がズームインしながら画面遷移するアプリは今や珍しくありません。
この表現を実現するライブラリはいくつか存在しますが、通常の`UINavigationController`のようにスワイプで戻れなくなったり、スワイプできても通常のスワイプとは違って指の動きに同期しないものが多い印象です。
iQONのアイテム詳細ページではこのジェスチャー周辺の実装がしっかりできているので、OSSとして公開したら需要があるかもと思い、ライブラリ化に踏み切りました。
<img src="https://media.giphy.com/media/l0HlIHq5gOrlzVlO8/giphy.gif" width="250px" height="445px" />


# 特徴
## エッジスワイプ
通常の`UINavigationController`と同様に、エッジスワイプで前のViewControllerに戻る事ができます。
上記のアニメーションGIFを見ていただくとわかりやすいと思います。

## Objective-Cプロジェクトでも使えます
作成したクラスはすべて`Foundation`, `UIKit`のクラスを継承しているため、Objective-Cのコードからも利用できます。

# 使い方
このライブラリを使って画面遷移のアニメーションを実装するには3つのステップがあります。
1. UINavigationControllerDelegate設定
2. 画面遷移元のViewController設定
3. 画面遷移先のViewController設定

## 1. UINavigationControllerDelegate設定

`UINavigationControllerDelegate`で画面遷移対象のViewControllerをチェックしてアニメーションをします。
`ZoomNavigationControllerDelegate`オブジェクトを`delegate`に設定するだけです。

```swift
class NavigationController: UINavigationController {

    private let zoomNavigationControllerDelegate = ZoomNavigationControllerDelegate()

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        delegate = zoomNavigationControllerDelegate
    }
}
```

## 2. 画面遷移元のViewController設定

アニメーション対象の`UIImageView`を返したり、アニメーション中に元の画像を非表示にできるように画面遷移前後の`ZoomTransitionSourceDelegate`のメソッドを実装します。

```swift
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`のメソッドを実装します。

```swift
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](https://github.com/WorldDownTown/ZoomTransitioning/tree/master/Demo)プロジェクトがあるので、そちらもご覧ください。

# ライブラリの内部実装
## ズームアニメーションの仕組み
`UIViewControllerAnimatedTransitioning`プロトコルを採用した`ZoomTransitioning`が画像がズームするアニメーション処理を実行しています。

`UIViewControllerAnimatedTransitioning`による画面遷移アニメーションについては、下記のQiita記事が参考になったので、そちらをご覧ください。<br />
[[Objective-C] UIViewControllerの遷移をカスタマイズする - Qiita](http://qiita.com/edo_m18/items/da215d4c8a361e61d0d4)

## スワイプで戻る
`UIPercentDrivenInteractiveTransition`を継承し、`UIGestureRecognizerDelegate`を採用した`ZoomInteractiveTransition`というクラスがスワイプによる画面遷移を実現させています。

```swift
let zoomInteractiveTransition = ZoomInteractiveTransition()
let gesture = navigationController.interactivePopGestureRecognizer
gesture?.delegate = zoomInteractiveTransition
gesture?.addTarget(zoomInteractiveTransition, action: #selector(ZoomInteractiveTransition.handlePanGestureRecognizer(_:)))
```

`UINavigationController`の`interactivePopGestureRecognizer`というプロパティは`UIScreenEdgePanGestureRecognizer`クラスで、このGestureRecognizerが通常のエッジスワイプで戻る動作を可能にしています。
`ZoomInteractiveTransitioning`で`interactivePopGestureRecognizer`のジェスチャー処理を受け取ることで、`ZoomTransitioning`が実行するアニメーションを使ってエッジスワイプで戻れるようになります。

```swift
// UINavigationControllerDelegate
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return zoomInteractiveTransition.interactive ? zoomInteractiveTransition : nil
}
```

エッジスワイプのジェスチャーを受け取ってナビゲーションを戻るときだけ`zoomInteractiveTransition`を返して、指の動きを反映したインタラクティブな画面遷移をします。


```swift
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](https://github.com/WorldDownTown/ZoomTransitioning/pulls)をお待ちしております。