iOS App資産タブリニューアルを終えて

こんにちは。スマホアプリエンジニアの都築です。

今回のエンジニアブログでは、先日リニューアルしたiOS Appの「マネーフォワード」アプリの資産タブについて書かせていただきたいと思います。

資産タブのリニューアルの背景

今年の5月にiOS Appのフルリニューアルを行ったのですが、リニューアル時の資産タブはこんな感じでした。
asset_tab

面白い動きで評判も良かったのですが、なにせ動作が遅い…。
なぜ動作が遅かったかというと、以前の資産タブでは標準のUIパーツを使用せず、自前のUIパーツを利用していた為、例えばUINavigationControllerのPush/Popといったような画面遷移も自前で実装する必要がありました。
さらに実装の都合で、画面遷移後のオブジェクトもあらかじめ生成しておく必要がありました…。
つまり、資産タブの初期画面では、その画面に不要なオブジェクトもHidden状態で生成されているというリソースの無駄使いをしていました。

Hidden状態のオブジェクトをDCIntrospectというツールを使って表示してみると、オブジェクトが重なりあってヒドいことに…。
asset_tab_objects

また、リソースの無駄使いの観点からだけでなく、実装的にも今後新たな機能を追加していく際に、機能拡張が難しいということで思い切って資産タブをリニューアルすることになりました。

資産タブのリニューアル

新しい資産タブのデザインは、iOS Appフルリニューアルと同じく、Goodpatch のデザイナーの方に作成していただきました。

作成していただいた動きはこんな感じ。

new asset tab from マネーフォワード on Vimeo.

標準のUIパーツには無い動きなので一見難しそう(?)に見えますが、標準のUIに少し手を加えるだけで実現できてしまいます。
動作要素は大きく分けて2つ。
1. 金融機関のカードをタップして、カードの詳細を表示する動き
2. カードの詳細を表示した状態から、カードを元のリストに戻す動き

1.の動きをさらに分解すると、タップされたカードがUITableViewを飛び出して画面上部に移動する動きと、画面下から別Viewがモーダルとして表示される動きに分解できます。
モーダルの表示は一般的な実装なので割愛して、今回はタップされたカードがUITableViewを飛び出して画面上部に移動する動きにフォーカスします。

今回の資産タブは、前回の教訓を活かして標準のUITableViewをベースにしたので、各金融機関のカードはUITableViewCellとして実装しています。
UITableViewCellをタップした際のdidSelectRowAtIndexPath:メソッド内のコードの簡単な流れは、以下になります。
* indexPathからUITableViewCellの情報を取得
* UITableViewCell上のアニメーションの対象となるUIViewを取得
* 取得したUIViewをUIWindowにaddSubview
* UITableViewCellからアニメーションの対象となるUIViewをremoveFromSuperview
* UIWindowにaddSubviewしたviewを画面上部まで移動
* モーダルの表示が完了した時点で、モーダルにアニメーションの対象となるUIViewをaddSubview
* UIWindowからremoveFromSuperview

なぜ、UIWindowにaddSubviewしているのか?と思った方もいるかと思いますが、下から出てくるモーダルがカードに重なった場合、カードが隠れてしまう為、一度UIWindowにaddSubviewしてカードを最前面に表示した状態で移動しています。
逆に、UIWindowにaddSubviewしたままにしておくと、画面遷移先でさらにPush, Modal遷移をした際、UIWindowが常に最前面に表示されてしまう為、アニメーション完了後は、UIWindowから削除しています。

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 省略
    ...

    // カードを閉じる際に必要なので、インスタンス変数に保存
    mSelectedIndexPath = indexPath;

    // 最前面でUIViewを移動する為のUIWindowを取得
    UIWindow* mainWindow = (((MfAppDelegate*) [UIApplication sharedApplication].delegate).window);

    // タップされたindexPathに対応するUITableViewCellを取得
    cell = [tableView cellForRowAtIndexPath:indexPath];

    // UITableViewCell上のUIViewを取得
    mContainerView = (UIView *)[cell viewWithTag:CONTAINER_VIEW_TAG];

    // カードを閉じる際に必要なので、インスタンス変数に保存
    mOriginalFrameOnTable = mContainerView.frame;

    // UITableView内のrectを取得
    CGRect newFrame = [tableView rectForRowAtIndexPath:indexPath];

    // UITableView内のrectからrootViewのrectに変換
    mOriginalFrameOnWindow = [tableView convertRect:newFrame toView:[tableView superview]];

    // UIWindow上でアニメーションさせるため、rootViewのrectを設定
    mContainerView.frame = mOriginalFrameOnWindow;

    // UITableViewからアニメーション対象のUIViewを削除
    [mContainerView removeFromSuperview];

    // UIWindowにアニメーション対象のUIViewを追加
    [mainWindow addSubview:mContainerView];

    [UIView animateWithDuration:ANIMATION_DURATION animations:^{
        // 目的のrectまでアニメーション
        mContainerView.frame = mTargetFrameOnWindow;
    }];

    // 省略
    ...
}

上記の実装で、アニメーション対象のUIViewを画面上部までアニメーションさせたので、今度はモーダル側で処理を行います。
以下のモーダル側の実装では、表示完了時に呼び出されるviewDidAppear:メソッド内で、UIWindowからカードViewを削除し、Modalとして表示したUIViewにaddSubviewする処理を行っています。

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    // UIWindowを取得
    UIWindow* mainWindow = (((MfAppDelegate*) [UIApplication sharedApplication].delegate).window);

    // UIWindow上のカードViewを取得
    mContainerView = (UIView *)[mainWindow viewWithTag:CONTAINER_VIEW_TAG];

    // カードがタップされた際に、閉じる処理を行うためのGestureRecognizerを生成
    mTouchGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(touchCardView:)];

    // Gestureを追加
    [mContainerView addGestureRecognizer:mTouchGesture];

    // UIWindowから削除
    [mContainerView removeFromSuperview];

    // モーダルにアニメーション対象のUIViewを追加
    [mModalView addSubview:mContainerView];

    // 省略
    ...
}

上記の実装により、UITableView上に並んだカードViewがUITableViewから飛び出すというような、デザイナーさんにいただいたイメージに近いものができました。
カードを閉じる際の処理は、もうお分かりかもしれませんが、上記で行った処理を逆の順番で行うことになります。

さらに動きを追加したい場合

ちなみに、上記の実装例ではコードがややこしくなるので簡略化しましたが、実際のAppではオリジナルのデザインには無いちょっとしたカードの動きを追加しています。
実際のAppを触っていただくとお分かりになると思いますが、カードをタップした際にスプリングアニメーションを追加しています。
スプリングアニメーションの追加は、下記のENABLE_SPRING_ANIMATION定義時に呼び出されるUIViewの

animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:

メソッドを呼び出すことで簡単に追加できます。
スプリングアニメーションを効果的に追加することで、より使っていて楽しいという印象を与えることができるかも?

 #if defined(ENABLE_SPRING_ANIMATION)
    // スプリングアニメーションあり
    [UIView animateWithDuration:ANIMATION_DURATION
                          delay:0.0
         usingSpringWithDamping:DAMPING_RATIO
          initialSpringVelocity:SPRING_VELOCITY
                        options:UIViewAnimationOptionLayoutSubviews
                     animations:^{
                         // 目的のrectまでアニメーション
                         mContainerView.frame = mTargetFrameOnWindow;
                     } completion:^(BOOL finished) {
                     }];
 #else
    // スプリングアニメーションなし
    [UIView animateWithDuration:ANIMATION_DURATION animations:^{
        // 目的のrectまでアニメーション
        mContainerView.frame = mTargetFrameOnWindow;
    }];
 #endif

終わりに

今回はiOS Appの新しい資産タブのアニメーションについて書いてみました。
標準のUIパーツを使いつつも、少し手を加えることで面白みのある動きを追加することができます。また、標準のUIパーツを利用することで、当然のことですがリソースの無駄使いも無くなりスッキリしました!
皆さんも是非いろいろと試してみてはいかがでしょうか?
new_asset_tab_objects

マネーフォワードでは、こだわりのiOS/Androidアプリを一緒に創り上げていくエンジニアを募集しています!
みなさまのご応募お待ちしております!

マネーフォワード採用サイト
https://recruit.moneyforward.com/

Wantedly
https://www.wantedly.com/companies/moneyforward

Pocket