Delegateを使ったお気に入りボタン付きの横スクロールCollectionViewの実装

この記事は アイソルート Advent Calendar 2021 19日目の記事です。
こんにちは。モバイルソリューショングループのfujinami.mです。
CollectionViewのセルをタップする処理で、タップした領域によって処理を分岐したいことはよくあることだと思います。
今回はApp Storeで表示されているような横スクロールのCollectionViewの実装とタップ領域での処理分岐になります。
目次
- 横スクロールのCollectionViewを配置・表示する
- カスタムセルを実装する
- 実装したカスタムセルを横スクロールCollectionViewに表示する
- お気に入りボタンのイベント発火を親Viewで受けと取る
- 最後に
1.横スクロールのCollectionViewを配置・表示する
今回は画像タップでタップした画像が画面のImageViewに設定され、GoodボタンでUserDefaultsにお気に入り情報を保持するアプリを実装したいと思います。
画面は以下で、赤い部分がCollectionViewになります。(見やすいように色付けしてますが、実行時は色設定を消します。)
タップされた画像の拡大バージョンを画面上部に表示するイメージです。
CollectionViewに設定は特に不要です。高さの領域が0になっているとセルが表示できなくなるので、そこだけ注意してください。
【画面イメージ】

2.カスタムセルを実装する
次にCollectionViewに表示するカスタムセルを実装します。
必要な要素は「表示する画像」「お気に入りボタン」の2つです。
今回は「お気に入りボタンを押された時」と「画像を押された時」で処理を分けたいと思います。
・「New File」から「Cocoa Touch Class」を選択

・「Subclass of」で「UICollectionViewCell」を選択し、「Also create XIB file」をチェックをつけて「Next」
・XIBファイルに画像表示用のImageViewとお気に入りボタンを配置

【コード】
class SampleCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var fruitsImageView: UIImageView!
@IBOutlet weak var goodButton: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
}
//セルの画像設定
func setImage(fruits: String, isFavorite: Bool) {
//取得した画像を表示
fruitsImageView.image = UIImage.init(named: fruits)
//お気に入り登録状態によってボタンの画像を切り替える
if isFavorite {
goodButton.setImage(UIImage.init(systemName: "hand.thumbsup.fill"), for: .normal)
} else {
goodButton.setImage(UIImage.init(systemName: "hand.thumbsup"), for: .normal)
}
}
@IBAction func tapGoodButton(_ sender: Any) {
//ボタン押下時の処理
}
}
3.実装したカスタムセルを横スクロールCollectionViewに表示する
CollectionViewにカスタムセルを表示していきます。
UserDefaultsに保存する処理はまだない状態です。
画像のスクロールと、画像をタップした時に上部に拡大イメージが表示されるまで実装できていればOKです。
【コード】
class ViewController: UIViewController {
@IBOutlet weak var fruitsIMageView: UIImageView!
@IBOutlet weak var fruitsCollectionView: UICollectionView!
//表示する画像名
let fruitsList = ["apple", "grape", "strawberry", "banana", "orange", "cherry"]
override func viewDidLoad() {
super.viewDidLoad()
//CollectionViewのセットアップを行う
setupFruitsCollectionView()
}
func setupFruitsCollectionView() {
//CollectionViewの設定と作ったカスタムセルの設定をする
fruitsCollectionView.register(UINib(nibName: "SampleCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "SampleCollectionViewCell")
fruitsCollectionView.dataSource = self
fruitsCollectionView.delegate = self
//CollectionViewを横位一列のレイアウトに設定
let fruitsCollectionViewLayout = UICollectionViewFlowLayout()
//表示するセルのサイズを指定する
fruitsCollectionViewLayout.itemSize = CGSize(width: 150, height: 150)
//スクロール方向を横に設定する
fruitsCollectionViewLayout.scrollDirection = .horizontal
fruitsCollectionView.collectionViewLayout = fruitsCollectionViewLayout
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//CollectionViewに表示する件数
return fruitsList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//作成したカスタムセルを表示する
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SampleCollectionViewCell", for: indexPath)
if let cell = cell as? SampleCollectionViewCell {
//UserDefaultsに保存されているお気に入り状態を取得する
let isFavorite = UserDefaults.standard.bool(forKey: fruitsList[indexPath.row])
//セルに画像を設定する
cell.setImage(fruits: fruitsList[indexPath.row], isFavorite: isFavorite)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//セルがタップされた時の処理
fruitsIMageView.image = UIImage.init(named: fruitsList[indexPath.row])
}
}
4.お気に入りボタンのイベント発火を親Viewで受け取る
やることは3つです。
- protocolでDelegateを定義する
- カスタムセルで定義したDelegateを使用する
- 親Viewに定義したDelegateをextensionで拡張する
上記をうまく実行すれば親CollectionViewのセル内のイベント発火を親Viewで受け取る事ができます。
・protocolでDelegateを定義する
こちらは定義するだけなので簡単です。
今回は取得したイベントが一つなので、定義も一つで大丈夫です。
処理を増やしたい場合は定義を増やしてください。
protocol SampleCollectionViewCellDelegate: AnyObject {
func tapGoodButton(_ fruits: String, _ isFavorite: Bool)
}
・カスタムセルで定義したDelegateを使用する
カスタムセルでSampleCollectionViewCellDelegateを定義して、
お気に入りボタン押下の処理でtapGoodButtonを実行しましょう。
本来protocolの定義も別のファイルを作成するべきだと思いますが、今回は簡易実装ということで一緒のファイル内に定義します。
以下、カスタムセルの全文です。
protocol SampleCollectionViewCellDelegate: AnyObject {
func tapGoodButton(_ fruits: String, _ isFavorite: Bool)
}
class SampleCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var fruitsImageView: UIImageView!
@IBOutlet weak var goodButton: UIButton!
var displayFruits: String = ""
weak var sampleCollectionViewCellDelegate: SampleCollectionViewCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
}
//セルの画像設定
func setImage(fruits: String, isFavorite: Bool) {
displayFruits = fruits
//取得した画像を表示
fruitsImageView.image = UIImage.init(named: fruits)
//お気に入り登録状態によってボタンの画像を切り替える
if isFavorite {
goodButton.setImage(UIImage.init(systemName: "hand.thumbsup.fill"), for: .normal)
} else {
goodButton.setImage(UIImage.init(systemName: "hand.thumbsup"), for: .normal)
}
}
@IBAction func tapGoodButton(_ sender: Any) {
//お気に入り状態を取得
let isFavorite = UserDefaults.standard.bool(forKey: displayFruits)
//表示画像を切り替える
if isFavorite {
goodButton.setImage(UIImage.init(systemName: "hand.thumbsup"), for: .normal)
} else {
goodButton.setImage(UIImage.init(systemName: "hand.thumbsup.fill"), for: .normal)
}
//定義したDelegate
sampleCollectionViewCellDelegate?.tapGoodButton(displayFruits, isFavorite)
}
}
・親Viewに定義したDelegateをextensionで拡張する
親Viewで作成したDelegateを拡張定義します。
やり方はCollectionViewと同じです。
以下、親Viewの全文です。
class ViewController: UIViewController {
@IBOutlet weak var fruitsIMageView: UIImageView!
@IBOutlet weak var fruitsCollectionView: UICollectionView!
//表示する画像名
let fruitsList = ["apple", "grape", "strawberry", "banana", "orange", "cherry"]
override func viewDidLoad() {
super.viewDidLoad()
//CollectionViewのセットアップを行う
setupFruitsCollectionView()
}
func setupFruitsCollectionView() {
//CollectionViewの設定と作ったカスタムセルの設定をする
fruitsCollectionView.register(UINib(nibName: "SampleCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "SampleCollectionViewCell")
fruitsCollectionView.dataSource = self
fruitsCollectionView.delegate = self
//CollectionViewを横位一列のレイアウトに設定
let fruitsCollectionViewLayout = UICollectionViewFlowLayout()
fruitsCollectionViewLayout.itemSize = CGSize(width: 150, height: 150)
fruitsCollectionViewLayout.scrollDirection = .horizontal
fruitsCollectionView.collectionViewLayout = fruitsCollectionViewLayout
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//CollectionViewに表示する件数
return fruitsList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//CollectionViewに表示するセルの内容
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SampleCollectionViewCell", for: indexPath)
if let cell = cell as? SampleCollectionViewCell {
let isFavorite = UserDefaults.standard.bool(forKey: fruitsList[indexPath.row])
cell.setImage(fruits: fruitsList[indexPath.row], isFavorite: isFavorite)
cell.sampleCollectionViewCellDelegate = self
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//セルがタップされた時の処理
fruitsIMageView.image = UIImage.init(named: fruitsList[indexPath.row])
}
}
extension ViewController: SampleCollectionViewCellDelegate {
func tapGoodButton(_ fruits: String, _ isFavorite: Bool) {
//Goodボタンが押された時の処理
if isFavorite {
UserDefaults.standard.set(false, forKey: fruits)
} else {
UserDefaults.standard.set(true, forKey: fruits)
}
}
}
【実行結果】
以下のように「お気に入りボタン」と「セル」のタップ時処理が分岐できていればOKです。

5.最後に
Delegateの定義からCollectionViewCellタップ時の処理分岐まで実装致しました。
TableViewでも同じ要領で応用が聞くので使えるようになるととても便利でした。
今回はUserDefaultsの更新しかしていないため、無理やりな実装になってしまいましたが、
セルタップ時に画面遷移をしてボタン押下で別の処理をする実装もあると思うので、
少しでも実装が楽になればと思います。









