はじめに
普段はUIPageViewControllerを使うことが多かったですが、
UICollectionViewでもイケそうでした。👍
実装
ざっくりとした流れ
- データソースの取得
- Viewクラスへの反映
- アクションのハンドリング
データソースの取得
まずは、チュートリアルページに表示する情報を取得するメソッドを用意します。
今回のサンプルでは、UICollectionViewのデータソースになる情報に関してはプロジェクト内に用意したjsonファイルを読み込んで、Codableでパースしています。
static func requestTutorialInfo() -> [Infomation] {
guard let jsonFilePath = Bundle.main.path(forResource: "tutorial_Info", ofType: "json") else {
print("pathの取得に失敗")
return []
}
do {
let jsonData = try Data(contentsOf: URL(fileURLWithPath: jsonFilePath), options: .mappedIfSafe)
let mappedData = try JSONDecoder().decode(TutorialData.self, from: jsonData)
return mappedData.info
} catch let error as NSError {
print("ファイル情報の取得に失敗:\(error.localizedDescription)")
return []
}
}
struct TutorialData: Decodable {
let info: [Infomation]
}
struct Infomation: Decodable {
let pageNumber: Int
let title: String
let message: String
let theme: String
private enum CodingKeys: String, CodingKey {
case pageNumber = "number"
case title
case message
case theme
}
}
Viewクラスへの反映
取得したデータソースをUICollectionViewに反映します。
まずは、各チュートリアルページとして横スクロールさせる用のセルクラスを用意します。
(Xibファイルも用意)
※ 今回はどのページも同じレイアウトなので一つのセルクラスしか用意してませんが、各ページでレイアウトが違うものにしたい場合は、別途そちら用のセルクラスを用意すればいいと思います。
import UIKit
class InfoViewCell: UICollectionViewCell {
@IBOutlet weak private var themeImageView: UIImageView!
@IBOutlet weak private var titleLabel: UILabel!
@IBOutlet weak private var messageLabel: UILabel!
// 自身のページ番号を持つ
var pageNumber = 0
static var identifier: String {
return String(describing: self)
}
static func nib() -> UINib {
return UINib(nibName: identifier, bundle: .main)
}
func setInfo(_ info: Infomation?) {
titleLabel.text = info?.title
messageLabel.text = info?.message
pageNumber = info?.pageNumber ?? 0
if let info = info, let theme = ThemeType(rawValue: info.theme) {
themeImageView.image = UIImage(named: theme.imageName)
backgroundColor = theme.color
} else {
themeImageView.image = UIImage(named: "no_image")
backgroundColor = .gray
}
}
}
次はViewControllerクラス内で、UICollectionView用に各種設定 & レイアウトを実装します。
@IBOutlet weak private var collectionView: UICollectionView!
- 各種設定
// viewDidLoadなどのタイミングで以下を設定
collectionView.isPagingEnabled = true
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.dataSource = self
collectionView.delegate = self
// 用意したセルクラスの登録
collectionView.register(InfoViewCell.nib(),forCellWithReuseIdentifier:InfoViewCell.identifier)
- データソースの反映
extension TutorialViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// 取得したデータソースの個数を返す
return presenter.numberOfTutorialPages
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InfoViewCell.identifier, for: indexPath) as! InfoViewCell
// 取得したデータソースを各セルクラスにセットする
cell.setInfo(presenter.tutorialInfo(forItem: indexPath.item))
return cell
}
}
- レイアウトを定義
今回はUICollectionViewDelegateFlowLayoutでレイアウトを作成
collectionViewのframeはstoryBoard側でsuperViewと同じサイズになるようにしています。
extension TutorialViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.bounds.size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return .zero
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return .zero
}
}
アクションのハンドリング
最後に「チュートリアルページの切り替え」に関する各種ハンドリング処理を用意します。
※ 前提として、このサンプルはMVPなのでページ番号の管理などはpresenterが行なっています。
- ボタンタップ or ページコントロールタップによるページシング
@IBAction func didTapForwardButton(_ sender: UIButton) {
// presenterへタップアクションを伝え、ページング処理を行う
}
@IBAction func didTapPageControl(_ sender: UIPageControl) {
// presenterへタップアクションを伝え、ページング処理を行う
}
タップアクションによるページングはUICollectionViewの以下のメソッドを使用します。
open func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionView.ScrollPosition, animated: Bool)
func pagingInfoList(_ newPage: Int) {
DispatchQueue.main.async { [weak self] in
self?.collectionView.scrollToItem(at: IndexPath(item: newPage, section: 0), at: .init(), animated: true)
}
}
- スクロールによるページシング
ページング処理自体はユーザーアクションにより完結しますが、今回は「現在のページ番号」を管理するためにアクションをハンドリングしています。
今回はUICollectionViewDelegateがUIScrollViewDelegateに準拠しているので、以下のメソッドで拾っています。
optional func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
extension TutorialViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// スクロール完了時に、表示中のページ番号を取得する
if let collectionView = scrollView as? UICollectionView,
let currentInfoCell = collectionView.visibleCells.first as? InfoViewCell {
presenter.scrollViewDidEndDecelerating(currentPageNumber: currentInfoCell.pageNumber)
}
}
}
ソース
今回作ったサンプルのソースは以下のリポジトリにあります。
https://github.com/ddd503/Tutorial-Paging-Sample