こんにちは!
21年新卒のTAROです!
今回ブログを書く機会をいただき、iOSアプリでは必ず使うと言ってもいいUICollectionViewについて書こうと思います。
ある案件で縦横スクロール可能なコレクションビューを実装する機会があったので、自分がどのように実装したか紹介します。
ちなみに動作は動画のような感じになります。
ボタンをタップすることで該当のセルに移動する感じです。
実装方法の概要
今回の実装は大まかに以下の順番で行いました。
UICollectionViewFlowLayoutを継承したクラスを定義する- 定義したクラスを使って
UICollectionViewを作成する - 作成したコレクションビューとボタンを画面に追加する
- ボタンをタップしたらコレクションビューの該当の場所に遷移するようにする
以下で詳しく紹介します。
UICollectionViewFlowLayout を継承したクラスを定義する
今回は縦横のスクロールを可能にするため、UICollectionViewFlowLayoutを継承したカスタムのクラスを定義します。
ちなみに、UICollectionViewFlowLayout はコレクションビューのレイアウトを管理するクラスです。
今回は以下のような実装を行いました。
詳しい実装内容についてはぜひ調べてみてください。
import UIKit
class CrossScrollLayout: UICollectionViewFlowLayout {
weak var delegate: UICollectionViewDelegateFlowLayout?
private var layoutInfo: [IndexPath: UICollectionViewLayoutAttributes] = [:]
private var itemSpacing: CGFloat = .zero
private var lineSpacing: CGFloat = .zero
private lazy var cellHeight: CGFloat = {
guard let collectionView = collectionView else { return .zero }
return collectionView.bounds.size.height
}()
private lazy var cellWidth: CGFloat = {
guard let collectionView = collectionView else { return .zero }
return collectionView.bounds.size.width
}()
private lazy var numberOfColumns: CGFloat = {
guard let collectionView = collectionView else { return .zero }
return CGFloat(collectionView.numberOfSections)
}()
private lazy var numberOfRows: CGFloat = {
guard let collectionView = collectionView else { return .zero }
return CGFloat(collectionView.numberOfItems(inSection: .zero))
}()
override func prepare() {
guard let collectionView = collectionView else { return }
delegate = collectionView.delegate as? UICollectionViewDelegateFlowLayout
setupLayoutInfo()
}
override var collectionViewContentSize: CGSize {
let contentWidth: CGFloat = itemSpacing * (numberOfColumns - 1) + cellWidth * numberOfColumns
let contentHeight: CGFloat = lineSpacing * (numberOfRows - 1) + (cellHeight * numberOfRows)
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var allAttributes: [UICollectionViewLayoutAttributes] = []
for attributes in layoutInfo.values {
if rect.intersects(attributes.frame) {
allAttributes.append(attributes)
}
}
return allAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutInfo[indexPath]
}
private func setupLayoutInfo() {
guard
let collectionView = collectionView,
let delegate = delegate
else { return }
var cellLayoutInfo: [IndexPath: UICollectionViewLayoutAttributes] = [:]
var originY: CGFloat = .zero
for section in 0..<collectionView.numberOfSections {
var height: CGFloat = .zero
var originX: CGFloat = .zero
for item in 0..<collectionView.numberOfItems(inSection: section) {
let indexPath = IndexPath(item: item, section: section)
let itemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let itemSize = delegate.collectionView?(
collectionView,
layout: self,
sizeForItemAt: indexPath
) ?? .zero
itemSpacing = delegate.collectionView?(
collectionView,
layout: self,
minimumInteritemSpacingForSectionAt: section
) ?? .zero
itemAttributes.frame = CGRect(
x: originX,
y: originY,
width: itemSize.width,
height: itemSize.height
)
cellLayoutInfo[indexPath] = itemAttributes
originX += (itemSize.width + itemSpacing)
height = height > itemSize.height ? height : itemSize.height
}
lineSpacing = delegate.collectionView?(
collectionView,
layout: self,
minimumLineSpacingForSectionAt: section
) ?? .zero
originY += (height + lineSpacing)
}
self.layoutInfo = cellLayoutInfo
}
}
コレクションビューとボタンを画面に追加する
先程作成したレイアウトクラスを使いコレクションビューを作成します。
また、コレクションビューとボタンを画面に追加します。
今回は、storyboardは使わずにSnapkitを使ってコードのみでUIを作成しました。
import UIKit
class HomeViewController: UIViewController {
private lazy var topButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "arrow.up"), for: .normal)
button.layer.borderColor = UIColor.link.cgColor
button.layer.borderWidth = 2
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(onTapTopButton(_:)), for: .touchUpInside)
return button
}()
private lazy var leftButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "arrow.left"), for: .normal)
button.layer.borderColor = UIColor.link.cgColor
button.layer.borderWidth = 2
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(onTapLeftButton(_:)), for: .touchUpInside)
return button
}()
private lazy var bottomButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "arrow.down"), for: .normal)
button.layer.borderColor = UIColor.link.cgColor
button.layer.borderWidth = 2
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(onTapBottomButton(_:)), for: .touchUpInside)
return button
}()
private lazy var rightButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "arrow.right"), for: .normal)
button.layer.borderColor = UIColor.link.cgColor
button.layer.borderWidth = 2
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(onTapRightButton(_:)), for: .touchUpInside)
return button
}()
private lazy var collectionView: UICollectionView = {
let layout = CrossScrollLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .clear
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(
ItemCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ItemCollectionViewCell.self)
)
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
initUI()
}
// MARK: - Private Methods
private func initUI() {
title = "HOME"
view.backgroundColor = .white
view.addSubview(collectionView)
view.addSubview(topButton)
view.addSubview(leftButton)
view.addSubview(bottomButton)
view.addSubview(rightButton)
collectionView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
make.left.equalTo(view.safeAreaLayoutGuide.snp.left)
make.right.equalTo(view.safeAreaLayoutGuide.snp.right)
}
topButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(20)
make.width.height.equalTo(40)
}
leftButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(view.safeAreaLayoutGuide.snp.left).offset(20)
make.width.height.equalTo(40)
}
bottomButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-20)
make.width.height.equalTo(40)
}
rightButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalTo(view.safeAreaLayoutGuide.snp.right).offset(-20)
make.width.height.equalTo(40)
}
}
@objc private func onTapTopButton(_ sender: UIButton) {
collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
@objc private func onTapBottomButton(_ sender: UIButton) {
let targetY = (collectionView.bounds.height + 10) * 2
collectionView.setContentOffset(CGPoint(x: 0, y: targetY), animated: true)
}
@objc private func onTapRightButton(_ sender: UIButton) {
let targetX = (collectionView.bounds.width + 10) * 2
collectionView.setContentOffset(CGPoint(x: targetX, y: 0), animated: true)
}
@objc private func onTapLeftButton(_ sender: UIButton) {
collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
@objc private func orientationDidChange() {
collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
}
// MARK: - UICollectionViewDataSource
extension HomeViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
10
}
func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
6
}
func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
guard
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ItemCollectionViewCell.self), for: indexPath) as? ItemCollectionViewCell
else { return UICollectionViewCell() }
let number = indexPath.section * 10 + indexPath.item
cell.setup(String(number))
return cell
}
}
// MARK: - UICollectionViewDelegate
extension HomeViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension HomeViewController: UICollectionViewDelegateFlowLayout {
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
return view.safeAreaLayoutGuide.layoutFrame.size
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int
) -> CGFloat {
return 10
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int
) -> CGFloat {
10
}
}
ボタンをタップした時の処理を記述する
ボタンをタップした時にコレクションビューの任意の場所に遷移するようにします。
setContentOffset(_:animated:) を使います。
@objc private func onTapTopButton(_ sender: UIButton) {
collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
@objc private func onTapBottomButton(_ sender: UIButton) {
let targetY = (collectionView.bounds.height + 10) * 2
collectionView.setContentOffset(CGPoint(x: 0, y: targetY), animated: true)
}
@objc private func onTapRightButton(_ sender: UIButton) {
let targetX = (collectionView.bounds.width + 10) * 2
collectionView.setContentOffset(CGPoint(x: targetX, y: 0), animated: true)
}
@objc private func onTapLeftButton(_ sender: UIButton) {
collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
上記のコードでボタンタップ時にCollectionViewの任意の場所に遷移できます。
まとめ
今回は縦横スクロールできるコレクションビューの実装例を紹介してみました。
縦スクロールできるコレクションビューはデフォルトの挙動で実現できるのですが、
横スクロールが入った途端に難しくなるのが新たな発見でした。
また機会があれば他の要件について、どのようなアプローチがあるのか調べてみようと思います。
最後まで読んでいただきありがとうございます!
投稿者プロフィール



