この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
一覧画面のレイアウトは同じデータでも見せ方ひとつで雰囲気が変わってきます。リスト表示は画像+文字情報の組み合わせたような見せ方に対して、グリッド表示は画像をメインにしたケースが多く見受けられます。リスト表示よりグリッド表示の方がサムネイルなどの画像が大きく表示されるのではないでしょうか。今回はそんなリスト表示とグリット表示をボタンひとつで切り替えられるDisplaySwitcherを試してみました。
UICollectionViewのレイアウトとカスタムトランジションを実装しており、ライセンスはMITです。
検証環境
今回は下記環境で試しています。
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を配置し、適宜制約をつけます。
また、切り替え用のボタンを配置します。DisplaySwitcherには切り替え用のボタンとしてSwitchLayoutButtonが用意されており、UIButtonとして配置します。ナビゲーションバーに配置する場合はUIViewを配置してその中に入れると良いかもしれません。
SwitchLayoutButtonを使う場合は、配置したボタンに対してカスタムクラスを設します。
Outlet接続をする
それぞれ、CollectionView、ボタンとボタンを押した時のアクションをOutlet接続します。
カスタムセルを作成する
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
サンプルコード
ViewController.swift
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
}
}
実行結果
シミュレーターで実行しました。
もし実行時に表示が崩れる場合は、ViewControllerのAdjust Scroll View Insetsのチェックを外しておきましょう。
さいごに
UICollectionViewのレイアウトとカスタムトランジションの実装は地味に面倒(個人の所感です)なので、リストを切り替えたい要件があった場合は楽になるのではと思います。 今回はリストとグリッドのセルを分けましたが、一つのセルにしてレイアウトの切り替えをすれば、もっと滑らかな切り替えになると思われます。