UICollectionViewのカスタムセルの作り方をご紹介します。
また、UICollectionViewの効率的な処理を支えるセルの再利用について、その仕組みおよびカスタムセル実装時の注意点についても触れます。
カスタムセルを利用したUICollectionViewの実装
最終的に作成する画面は以下のようなものです。
UILabelを縦に2つ並べただけのセルです。
後半のセルの再利用の仕組みについて説明するのに都合の良い画面になってます
StoryBoardの設定
まずはStoryBoardから。
ViewControllerにUICollectionViewを配置します。
UICollectionViewには上下左右にSafeAreaに対する制約をつけています。
UICollectionView内のセルはここでは削除しておきます。
カスタムセル用のXIBファイルの作成
次はカスタムセルのxibファイルです。
XCodeメニューのFile > New > File...とたどり、Cocoa Touch Classを選択します。
クラスの型をUICollectionViewCellにして、名前をつけます(ここではCustomCell
という名前にします)。
Also create XIB fileにチェックを付けておいてください。
CustomCell.xibというファイルが作成されたら、適当にUILabelを2つ配置した以下のような画面を作成します。
ViewControllerの実装
続いてViewControllerの実装です。
collectionView.register(UINib(nibName: "CustomCell", bundle: nil), forCellWithReuseIdentifier: "CustomCell")
まず、コレクションビューで使用するセルをregister(_:forCellWithReuseIdentifier:)
メソッドで登録します。
第一引数にはUINibオブジェクトを渡します。
UINibのイニシャライザに渡しているCustomCell
という文字列は、セルのXIBファイル名と一致します。
第二引数のforCellWithReuseIdentifier
には、セルの再利用識別子というものを指定します。
ここの文字列は任意ですが、ここではCustomCell
を指定します。
続いてデータソースの実装です。
ViewControllerにUICollectionViewDataSourceプロトコルを実装します。
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return models.count
}
collectionView(_:numberOfItemsInSection:)
メソッドでは、コレクションビューに配置するセルの個数を返すようにします。
models
はセルに表示するデータを持つモデルオブジェクトの配列です。
モデルオブジェクトについてはあとで触れます。
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath)
if let cell = cell as? CustomCell {
cell.setupCell(model: models[indexPath.row])
}
return cell
}
collectionView(_:cellForItemAt:)
メソッドでは、セルの設定を行います。
dequeueReusableCell(withReuseIdentifier:for:)
メソッドでセルオブジェクトを取り出し、セルにモデルオブジェクトを渡して設定を行っています。
新しくCustomCellオブジェクトを生成するのではなく、dequeueReusableCell(withReuseIdentifier:for:)
メソッドでキューに溜まっているセルオブジェクトを再利用するというところがポイントです。
あとで詳しく触れます。
全体のViewControllerの実装は以下のとおりです。
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
let models = Model.createModels()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.register(UINib(nibName: "CustomCell", bundle: nil), forCellWithReuseIdentifier: "CustomCell")
// セルの大きさを設定
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: collectionView.frame.width, height: 100)
collectionView.collectionViewLayout = layout
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return models.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath)
if let cell = cell as? CustomCell {
cell.setupCell(model: models[indexPath.row])
}
return cell
}
}
カスタムセルクラスの実装
カスタムセルのクラスの実装は以下のとおりです。
import UIKit
class CustomCell: UICollectionViewCell {
@IBOutlet weak var title: UILabel!
@IBOutlet weak var subTitle: UILabel!
func setupCell(model: Model) {
title.text = model.title
if let text = model.subTitle {
subTitle.text = text
}
self.backgroundColor = .lightGray
}
}
title
とsubTitle
はXIBファイル上のUILabelオブジェクトとリンクしています。
setupCell(model:)
メソッドはモデルオブジェクトを受け取って、2つのラベルの設定を行います。
モデルクラスの実装
最後にモデルのクラスです。
struct Model {
let title: String
let subTitle: String?
static func createModels() -> [Model] {
return [
Model(title: "AAAAAAAAAA", subTitle: "aaaaaaaaaa"),
Model(title: "BBBBBBBBBB", subTitle: "bbbbbbbbbb"),
Model(title: "CCCCCCCCCC", subTitle: "cccccccccc"),
Model(title: "DDDDDDDDDD", subTitle: "dddddddddd"),
Model(title: "EEEEEEEEEE", subTitle: "eeeeeeeeee"),
Model(title: "FFFFFFFFFF", subTitle: "ffffffffff"),
Model(title: "GGGGGGGGGG", subTitle: "gggggggggg"),
Model(title: "HHHHHHHHHH", subTitle: "hhhhhhhhhh"),
Model(title: "IIIIIIIIII", subTitle: "iiiiiiiiii"),
Model(title: "JJJJJJJJJJ", subTitle: "jjjjjjjjjj"),
Model(title: "KKKKKKKKKK", subTitle: "kkkkkkkkkk"),
Model(title: "LLLLLLLLLL", subTitle: "llllllllll"),
Model(title: "MMMMMMMMMM", subTitle: "mmmmmmmmmm"),
Model(title: "NNNNNNNNNN", subTitle: nil),
Model(title: "OOOOOOOOOO", subTitle: nil),
Model(title: "PPPPPPPPPP", subTitle: nil),
Model(title: "QQQQQQQQQQ", subTitle: nil),
Model(title: "RRRRRRRRRR", subTitle: nil),
Model(title: "SSSSSSSSSS", subTitle: nil),
Model(title: "TTTTTTTTTT", subTitle: nil),
Model(title: "UUUUUUUUUU", subTitle: nil),
Model(title: "VVVVVVVVVV", subTitle: nil),
Model(title: "WWWWWWWWWW", subTitle: nil),
Model(title: "XXXXXXXXXX", subTitle: nil),
Model(title: "YYYYYYYYYY", subTitle: nil),
Model(title: "ZZZZZZZZZZ", subTitle: nil),
]
}
}
モデルはtitle
とsubTitle
という2つのプロパティを持っています。
subTitle
はオプショナル型なので、nilの可能性もあります。
createModels()
はコレクションビューに表示するデータを作成します。
ViewControllerはこのメソッドで作成したデータをmodels
プロパティに保持しています。
これらの実装でアプリを起動すると、以下のような画面になるはずです。
セルの再利用の仕組みについて
上で紹介したアプリはかなり作為的な実装になってますが、セルの再利用の仕組みについて説明するのに都合が良いのです。
上記アプリの問題点
画面を下にスクロールしていくと、ある時点からおかしなことが起こります。
NNNNNNNNNN
というタイトルの下に、aaaaaaaaaa
というサブタイトルが表示されています。
ViewControllerのmodels
には、Model(title: "NNNNNNNNNN", subTitle: nil)
というデータが入っているので、サブタイトルには何も表示されないのが期待している動作です。
セルは再利用されている
画面を下にスクロールしていくと、上にあるセルは表示されなくなり、下から新しいセルが表示されます。
この時、コレクションビューは新しいセルオブジェクトを作成するのではなく、表示されなくなったセルを内部的なキューに入れておき、新しいセルが表示されるときにキューからセルを取り出すという方法を取ります。
記事の最初の方で触れたdequeueReusableCell(withReuseIdentifier:for:)
というメソッドは、キューに溜まっている使われなくなったセルを取り出すためのものなのです。
キューはセルの種類ごとに用意されていて、再利用識別子を使ってどのキューからセルを取り出すかを決定します。
NNNNNNNNNN
というタイトルの下にaaaaaaaaaa
というサブタイトルが表示されていたのは、最初にAAAAAAAAAA
というタイトルを表示していたセルを再利用したからです。
セルは初期化してから使用する
セルを使用するときに毎回初期化を行うことで、このようは問題はなくなります。
UICollectionViewCellにはprepareForReuse()
というメソッドが用意されていて、これはセルがキューから取り出される際に呼ばれます。
以下のようにtitle
とsubTitle
に空文字列をセットしてあげることで、期待した動作になります。
override func prepareForReuse() {
super.prepareForReuse()
title.text = ""
subTitle.text = ""
}