こんにちは!
21年新卒のTAROです!
今回ブログを書く機会をいただき、iOSアプリでは必ず使うと言ってもいいUICollectionView
について書こうと思います。
ある案件で縦横スクロール可能なコレクションビューを実装する機会があったので、自分がどのように実装したか紹介します。
ちなみに動作は動画のような感じになります。
ボタンをタップすることで該当のセルに移動する感じです。
実装方法の概要
今回の実装は大まかに以下の順番で行いました。
UICollectionViewFlowLayout
を継承したクラスを定義する- 定義したクラスを使って
UICollectionView
を作成する - 作成したコレクションビューとボタンを画面に追加する
- ボタンをタップしたらコレクションビューの該当の場所に遷移するようにする
以下で詳しく紹介します。
UICollectionViewFlowLayout
を継承したクラスを定義する
今回は縦横のスクロールを可能にするため、UICollectionViewFlowLayout
を継承したカスタムのクラスを定義します。
ちなみに、UICollectionViewFlowLayout
はコレクションビューのレイアウトを管理するクラスです。
今回は以下のような実装を行いました。
詳しい実装内容についてはぜひ調べてみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
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を作成しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
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:)
を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@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の任意の場所に遷移できます。
まとめ
今回は縦横スクロールできるコレクションビューの実装例を紹介してみました。
縦スクロールできるコレクションビューはデフォルトの挙動で実現できるのですが、
横スクロールが入った途端に難しくなるのが新たな発見でした。
また機会があれば他の要件について、どのようなアプローチがあるのか調べてみようと思います。
最後まで読んでいただきありがとうございます!
投稿者プロフィール
最新の投稿
- iOS2022.08.02縦横スクロール可能なコレクションビューを実装する
- iOS2021.09.24個人開発したiOSアプリをAppStoreConnectにアップロードしてTestFlightを利用する
- WEB2021.05.14TypeScriptなVue.jsでWYSIWIGエディタを実装する