[iOS][Swift3.0] リストとグリットの表示切り替えが楽になるDisplaySwitcher

一覧画面のレイアウトは同じデータでも見せ方ひとつで雰囲気が変わってきます。リスト表示は画像+文字情報の組み合わせたような見せ方に対して、グリッド表示は画像をメインにしたケースが多く見受けられます。リスト表示よりグリッド表示の方がサムネイルなどの画像が大きく表示されるのではないでしょうか。今回はそんなリスト表示とグリット表示をボタンひとつで切り替えられるDisplaySwitcherを試してみました。

intro

UICollectionViewのレイアウトとカスタムトランジションを実装しており、ライセンスはMITです。

Yalantis/DisplaySwitcher

検証環境

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

Xcode 8.2.1
Swift 3.0.2
CocoaPods 1.2.0

準備

導入

CocoaPodsで追加します。

platform :ios, '9.0'
use_frameworks!

target 'ターゲット名' do
    pod 'DisplaySwitcher', '~> 1.0'
end

実装

Storyboardに配置する

UICollectionViewを配置し、適宜制約をつけます。

001

また、切り替え用のボタンを配置します。DisplaySwitcherには切り替え用のボタンとしてSwitchLayoutButtonが用意されており、UIButtonとして配置します。ナビゲーションバーに配置する場合はUIViewを配置してその中に入れると良いかもしれません。

002

SwitchLayoutButtonを使う場合は、配置したボタンに対してカスタムクラスを設します。

003

Outlet接続をする

それぞれ、CollectionView、ボタンとボタンを押した時のアクションをOutlet接続します。

005

カスタムセルを作成する

UICollectionViewCellを作成します。今回は、リスト用とグリッド用の.xibファイルをそれぞれ作成しました。

セルの作成

注意点としてそれぞれのCellの高さは固定になります。それ以外は通常通りに作成して大丈夫です。

コードの実装

DisplaySwitcherをインポートします。

import DisplaySwitcher

リストとグリッドのそれぞれのセルの高さを定義します。

// リスト表示時のセルの高さ
private let listCellHeight: CGFloat = 88
// グリッド表示時のセルの高さ
private let gridCellHeight: CGFloat = 168

2つのレイアウトを作成します。

private lazy var listLayout = DisplaySwitchLayout(staticCellHeight: listCellHeight, nextLayoutStaticCellHeight: gridCellHeight, layoutState: .list)
private lazy var gridLayout = DisplaySwitchLayout(staticCellHeight: gridCellHeight, nextLayoutStaticCellHeight: listCellHeight, layoutState: .grid)

レイアウトの状態を保持する変数と、CollectionViewに対して初期に表示するレイアウトを設定します。

private var layoutState: LayoutState = .list
collectionView.collectionViewLayout = listLayout
// nibで作成した場合は登録しておく
collectionView.register(UINib(nibName: "ListCell", bundle: nil), forCellWithReuseIdentifier: "listCell")
collectionView.register(UINib(nibName: "GridCell", bundle: nil), forCellWithReuseIdentifier: "gridCell")

SwitchLayoutButtonを使用している場合は初期化します。

private var layoutState: LayoutState = .list
collectionView.collectionViewLayout = listLayout

UICollectionViewDataSource 2つのメソッドをオーバーライドして適宜実装します。

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    // リストの件数を返す
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    // リストとグリッドの状況に応じたセルを返す
}

カスタムレイアウトの切り替え用にUICollectionViewDelegateの下記メソッドをオーバーライドして下記コードを実装します

func collectionView(_ collectionView: UICollectionView, transitionLayoutForOldLayout fromLayout: UICollectionViewLayout, newLayout toLayout: UICollectionViewLayout) -> UICollectionViewTransitionLayout {
    let customTransitionLayout = TransitionLayout(currentLayout: fromLayout, nextLayout: toLayout)
    return customTransitionLayout
}

ボタンをタップした時など、リストの切替時に下記コードを実装します。

let transitionManager: TransitionManager
if layoutState == .list {
    layoutState = .grid
    transitionManager = TransitionManager(duration: animationDuration, collectionView: collectionView!, destinationLayout: gridLayout, layoutState: layoutState)
} else {
    layoutState = .list
    transitionManager = TransitionManager(duration: animationDuration, collectionView: collectionView!, destinationLayout: listLayout, layoutState: layoutState)
}
transitionManager.startInteractiveTransition()
// SwitchLayoutButtonを使用している場合は以下コードを記述する
switchButton.isSelected = layoutState == .list
switchButton.animationDuration = animationDuration

サンプルコード

import UIKit
import DisplaySwitcher

// リスト表示時のセルの高さ
private let listCellHeight: CGFloat = 88
// グリッド表示時のセルの高さ
private let gridCellHeight: CGFloat = 168

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var switchButton: SwitchLayoutButton!

    fileprivate let listCell = "ListCell"
    fileprivate let gridCell = "GridCell"

    private let animationDuration: TimeInterval = 0.3

    fileprivate lazy var listLayout = DisplaySwitchLayout(staticCellHeight: listCellHeight, nextLayoutStaticCellHeight: gridCellHeight, layoutState: .list)
    fileprivate lazy var gridLayout = DisplaySwitchLayout(staticCellHeight: gridCellHeight, nextLayoutStaticCellHeight: listCellHeight, layoutState: .grid)
    
    fileprivate var layoutState: LayoutState = .list
    fileprivate var isTransitionAvailable = true

    override func viewDidLoad() {
        super.viewDidLoad()

        setupCollectionView()
        setupSwitchButton()
    }

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

    // MARK: - Action
    @IBAction func didTapSwitchButton(_ sender: Any) {
        let transitionManager: TransitionManager
        if layoutState == .list {
            layoutState = .grid
            transitionManager = TransitionManager(duration: animationDuration, collectionView: collectionView!, destinationLayout: gridLayout, layoutState: layoutState)
        } else {
            layoutState = .list
            transitionManager = TransitionManager(duration: animationDuration, collectionView: collectionView!, destinationLayout: listLayout, layoutState: layoutState)
        }
        transitionManager.startInteractiveTransition()
        switchButton.isSelected = layoutState == .list
        switchButton.animationDuration = animationDuration
    }

    // MARK: - Private
    private func setupCollectionView() {
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.collectionViewLayout = listLayout
        collectionView.register(UINib(nibName: listCell, bundle: nil), forCellWithReuseIdentifier: listCell)
        collectionView.register(UINib(nibName: gridCell, bundle: nil), forCellWithReuseIdentifier: gridCell)
    }

    private func setupSwitchButton() {
        switchButton.isSelected = layoutState == .list
        switchButton.animationDuration = animationDuration
    }
}

// MARK: - UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // 件数を返す
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        if layoutState == .grid {
            guard let gridCell = collectionView.dequeueReusableCell(withReuseIdentifier: gridCell, for: indexPath) as? GridCell else {
                fatalError("Could not create GridCell")
            }
            // データをセットするコードを実装する
            return gridCell
        }
        guard let litCell = collectionView.dequeueReusableCell(withReuseIdentifier: listCell, for: indexPath) as? ListCell else {
            fatalError("Could not create ListCell")
        }
        // データをセットするコードを実装する
        return litCell
    }    
}

// MARK: - UICollectionViewDelegate
extension ViewController: UICollectionViewDelegate {

    func collectionView(_ collectionView: UICollectionView, transitionLayoutForOldLayout fromLayout: UICollectionViewLayout, newLayout toLayout: UICollectionViewLayout) -> UICollectionViewTransitionLayout {
        let customTransitionLayout = TransitionLayout(currentLayout: fromLayout, nextLayout: toLayout)
        return customTransitionLayout
    }
}

実行結果

シミュレーターで実行しました。

sample

もし実行時に表示が崩れる場合は、ViewControllerのAdjust Scroll View Insetsのチェックを外しておきましょう。

チェックを外す

さいごに

UICollectionViewのレイアウトとカスタムトランジションの実装は地味に面倒(個人の所感です)なので、リストを切り替えたい要件があった場合は楽になるのではと思います。 今回はリストとグリッドのセルを分けましたが、一つのセルにしてレイアウトの切り替えをすれば、もっと滑らかな切り替えになると思われます。