【iOS】Pinterest風な遷移アニメーションをオープンソースとして公開しました

大島 雅人
255

概要

UIViewController間の遷移アニメーションを、Pinterestのようなズームするアニメーションにするライブラリをオープンソースとして公開しました。前回オープンソースを紹介したときはyshrkt氏のゴーストライターでしたが、今回は自分で書いたコードですw

We published RMPZoomTransitionAnimator by open source.
RMPZoomTransitionAnimator provide a custom transition zooming animation.

PinterestやFacebook、Twitterなどのアプリで、一覧画面で写真のサムネイルをクリックするとサムネイルが拡大しながら詳細画面に遷移するアニメーションをご覧になったことがあると思いますが、あのアニメーションを再現しようとしたライブラリです。あくまでも簡単にそれっぽいものを再現するためのものなので完全に再現できている訳ではありません。
Githubで公開して3日ほど経ちましたが、Starが70以上ついていてObjective-CジャンルでGithubのTrending repositoriesにも(Dailyですが)のりました

弊社の料理サプリのiOSでも使用しています。gifアニメをご覧頂ければどんなものか分かるかと思います。

out

特徴

Pinterestっぽいアニメーションと言えばだいたいの開発者の方は分かるかと思います。Githubで検索するとPinterestAnimatorというまんまの名前のライブラリが見つかるんですが、Pinterestを完全に再現しようとしていてデザインが既に決まっているプロジェクトには取り込みにくいという問題点がありました。この問題点を解決するために最初からオープンソースにするつもりで切り出せるような形で開発を行いました。

UICollectionView以外でも使える

collection view gifアニメ

table view gifアニメ

実際のプロジェクトだと既にViewControllerはかなりカスタマイズされていて、UICollectionViewを持つViewControllerのView構造を変えたり、UITableViewUICollectionViewに作り変えたり、新しいクラスを継承したりというのはなかなか難しいと思います。構造を変えるまではしたくないけれど、手軽にアニメーションだけを取り入れたいという時にこのライブラリの使い所があるんじゃないかなと思います。

UIViewControllerAnimatedTransitioningの各メソッドを実装すればアニメーションは簡単に実装できます。アニメーション処理を書く際に、遷移前と遷移後のViewControllerを取得できるので必要な情報はViewControllerから取得する設計にしました。実装すべきメソッドは、プロトコルとして宣言して各ViewControllerに実装してもらうような形にし、アニメーション処理に使う画像のフレーム計算などはライブラリ側では持たないようにして、利用者がどんなViewControllerにも適用できるようにしてあります。

ズームアニメーションの実現方法

アニメーションを行うanimateTransition:(id)transitionContextで遷移前のViewControllerと遷移後のViewControllerからズームアニメーションを行う画像や遷移後にぴったりとはまる位置を取得するようにしています。だいたいのイメージを下の画像にまとめています。この移動させる画像や位置は先ほど述べたように、ライブラリの利用者側に全て委ねています。どんな風に取得すればいいかというのは、ライブラリとは関係ないのですがコード自体はExampleプロジェクトで書いているので、参考になるかと思います。

ズームアニメーションについてですが、アニメーションの秒数やアニメーションカーブにこだわりふわっと着地するように感じるように工夫しました。開発しながら一人で何度もアニメーションを見ていると、何がベストかよく分からなくなってきましたが、開発者同士でレビューし合っていいものが出来たと思っています。

structure

ライブラリの使い方

READMEにも書いていますが、cocoapods経由でインストールできます。まぁライブラリ自体は1つのクラスなので、わざわざcocoapods経由でインストールする必要もないかもしれません。実装の参考にだけして、自分たちでアニメーション部分だけを取り入れるのもいいと思います。

RMPZoomTransitionAnimatingの実装

登場人物としては、遷移前のリストやコレクションなどの一覧側のViewControllerと、遷移後の詳細画面が必要です。どちらのViewControllerのヘッダファイルにも、RMPZoomTransitionAnimator.hをimportしたあとにRMPZoomTransitionAnimatingプロトコルの適合を宣言します。

次に、実装ファイルにプロトコルを実装していきます。
プロトコルで宣言している実装すべきメソッドは3つあります。

  1. 拡大縮小アニメーションをするUIImageViewを設定する ((UIImageView *)transitionSourceImageView)
  2. 遷移前の画面のbackgroundColorを設定する ((UIColor *)transitionSourceBackgroundColor)
  3. 拡大縮小アニメーションをするUIImageViewが最終的におさまるframeを設定する ((CGRect)transitionDestinationImageViewFrame)

1で注意して欲しいのは、UIImageViewを作り直す必要があるということです。ここで指定したUIImageViewを実際に移動させていくので、もとのUIImageViewのインスタンスを使ってしまうと消えてしまいます。なので、コピーを用意してください。
2は見た目の問題なのですが、遷移前のbackgroundColorを使ってフェードインアニメーションを自然なものにするために使っています。ただユーザーに見えている色が単にself.view.backgroundColorとは限らない(self.collectionView.backgroundColorかもしれない)ので、プロトコル経由で取得するようにしています。
3は単純でアニメーションする画像が遷移後におさまる最終的なフレームを返せばOKです。

RMPZoomTransitionAnimatorインスタンスの設定

最後に、iOSが用意しているカスタム遷移アニメーションに関するdelegateメソッドでRMPZoomTransitionAnimatorを生成して返却するわけですが、UINavigationControllerのPush、Popアニメーションに適用する場合とModalで表示する場合で設定するdelegateが変わってきます。

UINavigationControllerの場合

UINavigationControllerDelegateの下記のdelegateメソッドで設定します。

- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC
{
    // minimum implementation for example
    RMPZoomTransitionAnimator *animator = [[RMPZoomTransitionAnimator alloc] init];
    animator.goingForward = (operation == UINavigationControllerOperationPush);
    animator.sourceTransition = (id<RMPZoomTransitionAnimating>)fromVC;
    animator.destinationTransition = (id<RMPZoomTransitionAnimating>)toVC;
    return animator;
}

Modalの場合

UIViewControllerTransitioningDelegateの下記のdelegateメソッドで設定します。

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source
{
    // minimum implementation for example
    RMPZoomTransitionAnimator *animator = [[RMPZoomTransitionAnimator alloc] init];
    animator.goingForward = YES;
    animator.sourceTransition = (id<RMPZoomTransitionAnimating>)source;
    animator.destinationTransition = (id<RMPZoomTransitionAnimating>)presented;
    return animator;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    // minimum implementation for example
    RMPZoomTransitionAnimator *animator = [[RMPZoomTransitionAnimator alloc] init];
    animator.goingForward = NO;
    animator.sourceTransition = (id<RMPZoomTransitionAnimating>)dismissed;
    animator.destinationTransition = (id<RMPZoomTransitionAnimating>)self;
    return animator;
}

詳細はREADMEとExampleで説明しているので、そちらをご参照ください。

ライセンス

MITライセンスです。

Copyright (c) 2015 Recruit Marketing Partners Co.,Ltd.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

余談: OSSで公開するということ

社内LT大会で弊社Androidエンジニアの平野氏もOSSで公開することのメリットについて言及していましたが、私も同感です。OSSで公開しようとすると必然的に疎結合になるように設計しなければならず、ついやりがちなプロジェクト固有な微調整が入っていないかを意識することができます。

一方で、デメリットという訳ではありませんが、READMEやソースコード中のコメントを英語で書く必要があり、自分の英語力がないのが悪いのですがある程度読めるような英語を心がけると時間が余計にかかってしまいます。
解決策とまではいきませんが、私の場合は、iOSのReferenceの英語を参考にすることが多いです。例えば、今回の場合は次の画面に遷移する場合と、戻る場合というのをフラグで確認する必要があったので、goingForwardというBOOL値を返すプロパティを作成しました。これのコメントをどうするか悩んだ訳ですが、同じように、開発者が設定可能なフラグはないか?と考えたときに、UIViewのuserInteractionEnabledというプロパティを思いつきます。
ドキュメントを見てみると、

A Boolean value that determines whether user events are ignored and removed from the event queue.

と書いてあるので、whetherあたりまではそのまま使えそうだな、その後ろ側だけ説明を変更すればいいかなとなるわけです。命名規則についてチームメンバーと雑談してたときにも、困った時はiOSのReferenceを参考にするよねと話していて、基本Referenceをベースに考え始めるとよさそうです。

まとめ

ついつい色々説明してしまいましたが、画面遷移アニメーションをオープンソースで公開したという話でした。
このコードを使っている料理サプリのiOSアプリバージョン3.3.0がもうすぐリリースされる予定です。プロの動画が見れる料理サプリはサービスのローンチから先月までで累積動画数1,685本、参加シェフ数が91名となりました!コンテンツ制作チーム、プロダクトオーナー、デザイナー、エンジニアみんなで協力しながら改善をし続けているプロダクトです。充実したコンテンツで溢れているので是非是非インストールしてみてください!
※ ちなみにGitHubにあげたExampleプロジェクトの写真は、エンジニアで共有しているランチ情報の写真を勝手に使いましたw
なんだか長くなってしまいましたが、最後まで読んでいただきありがとうございました。