[iOS][Swift3.0] グリーティングカードを開くようなエフェクトが素晴らしいFoldingCell

2017.02.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

FoldingCellはUITableViewCellを拡張して作られており、紙が折りたたまれるようなコンポーネントを実現することができます。 イメージはグリーティングカードをパタパタ開くような感じです。Ramotion Inc.という企業が作成しており、下記は企業ページに載っていた紹介のGIF画像です。

FoldingCell Ramotion Inc.

タップすると紙が開かれ、再度タップすると紙が閉じるようなエフェクトです。FoldingCellのライセンスはMITです。

Ramotion/folding-cell

検証環境

今回は下記環境で試しています。

Xcode 8.2.1
Swift 3.0.2
CocoaPods 1.0.0

準備

導入

CocoaPodsで追加します。

use_frameworks!
target "ターゲット名"
    pod 'FoldingCell'
end

post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |configuration|
            configuration.build_settings['SWIFT_VERSION'] = "3.0"
        end
    end
end

画面の用意

今回使うTableViewを用意しておきます。StoryboardにUITableViewControllerを配置し、クラスファイルを作成します。また、TableViewのStyleはGroupedにしておきます。

00準備

実装

FoldingCell用のCellはStoryboardまたはnibファイル、どちらでも作成できます。(今回はStoryboardの方に作成しました) 簡単に概要を書くと、セルの中に閉じている時のViewと開いている時のViewの両方を作成します。

概要図

FoldingCellを継承したクラスの作成

FoldingCellを継承したCellクラスを作成します。func animationDuration(_:type:)はoverrideして、値を返さないとエラーになりますが、他は特に何もしなくても大丈夫でした。下記コードではbackgroundColorを透明にしてますが、見た目(デザイン)の問題で必須ではありません。

SampleCell.swift

import UIKit
import FoldingCell

class SampleCell: FoldingCell {

    override func awakeFromNib() {
        super.awakeFromNib()

        backgroundColor = UIColor.clear
    }

    override func animationDuration(_ itemIndex:NSInteger, type:AnimationType)-> TimeInterval {
        let durations = [0.26, 0.2, 0.2]
        return durations[itemIndex]
    }
}

カスタムクラスの設定

上記で作成したクラス(ここではSampleCell)をCellに設定します。

00クラス設定

Viewの配置

Cellの高さを適宜ひろげて、閉じた時と開いた時用のViewを配置します。

002Viewを配置

わかりやすいように、閉じている時のViewを水色、開いている時のViewをピンク色にしています。

002配置後

閉じている時のViewにカスタムクラスを設定

閉じている時のView(画像の水色の方)のClassにRotatedViewを設定します。(開いている時のView(ピンクの方)は設定しなくて大丈夫です)

003RotatedViewを設定

配置の制約をつける

閉じている時のView

Top SpaceTrailing SpaceLeading SpaceHeight(高さ)に制約を付けます。

005制約

開いている時のView

こちらも同じように、Top SpaceTrailing SpaceLeading SpaceHeight(高さ)に制約を付けます。ここで注意するのは、Top SpaceSuperViewからのスペースにする必要があります。

006制約

Outlet接続

セルと各View、各ViewのTop Space制約をOutletで接続します。

接続するもの 接続先変数名 タイプ
閉じている時のView foregroundView RotatedView
開いている時のView containerView UIView
閉じている時のViewのTop Space制約 foregroundViewTop NSLayoutConstraint
開いている時のViewのTop Space制約 containerViewTop NSLayoutConstraint

007outlet

開いている時のView(containerView)内にViewを配置する

パタパタと開くためのViewをcontainerView内に配置します。

008ContainerViewにViewを配置する

今回は4つのViewを配置しました。(わかりやすいように、それぞれ背景色を変えています)

配置例

これらにレイアウトの制約を付けます。上下左右と高さを付けます、それぞのViewの間は0にしました。

制約

containerView内のViewにカスタムクラスの設定する

上記で追加したViewにカスタムクラスとしてRotatedViewを設定しますが、ここで注意するのが、偶数番目に配置したViewのみ設定します。今回は2番めと4番めのViewが該当します。

偶数のみ

Cellのプロパティ設定

FoldingCellのプロパティを設定します。
item Countにはパタパタ用に追加したViewの数(今回は4)、Back View Colorにはパタパタした時の裏側の色を設定します。(デフォルトだと茶色になります)

プロパティ設定

また、CellにIdentifierをつけておきます。

CellにIdentifierをつける

UITableViewControllerのコード

閉じた時と開いた時のセルの高さを定義する

それぞれの高さ+上下の余白分の高さをそれぞれ指定しましょう。

private let closeCellHeight: CGFloat = 96
private let openCellHeight: CGFloat = 328

セルの高さを保持しておく変数を定義する

それぞれのセルに対して高さの状態を保持するように配列を定義します。この配列の初期値は閉じている高さ(closeCellHeight)にしておきます。

private var cellHeights: [CGFloat] = []
override func viewDidLoad() {
    super.viewDidLoad()
    cellHeights = Array.init(repeating: closeCellHeight, count: cellCount) // cellCountはTableViewに表示するリストの件数
}

タップした時の処理

セルが選択されたタイミングで高さを見て開いたり閉じたりします。

cell.selectedAnimation(true, animated: true, completion: nil)

で開いた状態に、

cell.selectedAnimation(false, animated: true, completion: nil)

で閉じた状態になります。

サンプルコード

SampleTableViewController.swift

import UIKit

class SampleTableViewController: UITableViewController {

    private let closeCellHeight: CGFloat = 96
    private let openCellHeight: CGFloat = 328

    private let cellCount = 3

    private var cellHeights: [CGFloat] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        cellHeights = Array.init(repeating: closeCellHeight, count: cellCount)
        tableView.backgroundColor = UIColor.gray
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return cellCount
    }

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return cellHeights[indexPath.row]
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? SampleCell else {
            fatalError("Could not create SampleCell")
        }
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard case let cell as SampleCell = tableView.cellForRow(at: indexPath) else {
            return
        }

        var duration = 0.0
        if cellHeights[indexPath.row] == closeCellHeight { // open cell
            cellHeights[indexPath.row] = openCellHeight
            cell.selectedAnimation(true, animated: true, completion: nil)
            duration = 0.5
        } else {// close cell
            cellHeights[indexPath.row] = closeCellHeight
            cell.selectedAnimation(false, animated: true, completion: nil)
            duration = 1.1
        }

        UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: { _ in
            tableView.beginUpdates()
            tableView.endUpdates()
        }, completion: nil)
    }

    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if case let cell as SampleCell = cell {
            if cellHeights[indexPath.row] == closeCellHeight {
                cell.selectedAnimation(false, animated: false, completion:nil)
            } else {
                cell.selectedAnimation(true, animated: false, completion: nil)
            }
        }
    }
}

実行結果

何も配置していないので見た目は微妙ですが、折りたたむ表現を入れることができました。

実行結果

さいごに

FoldingCellを使うと、今まで画面を遷移して詳細を表示していたものが、同じ画面内で詳細を表示する導線に変更することができるなと思いました。
高さを動的にするのは無理そうな感じですが、表示量がそこまで多くなくて固定でレイアウトを作れるものであれば、ぴったりと合うのではないでしょうか。

Android版も公開されています✨
https://github.com/Ramotion/folding-cell-android