UICollectionViewでチュートリアル画面を作る

# はじめに
普段はUIPageViewControllerを使うことが多かったですが、
UICollectionViewでもイケそうでした。👍

<img width="300" alt="画像" src="https://github.com/ddd503/Image-Resource/raw/master/tutorial.gif">

# 実装

ざっくりとした流れ

 - データソースの取得
 - Viewクラスへの反映
 - アクションのハンドリング

#### データソースの取得
まずは、チュートリアルページに表示する情報を取得するメソッドを用意します。
今回のサンプルでは、UICollectionViewのデータソースになる情報に関してはプロジェクト内に用意したjsonファイルを読み込んで、Codableでパースしています。

```TutorialDataStore.swift
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 []
        }
    }
```

```Infomation.swift
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ファイルも用意)
※ 今回はどのページも同じレイアウトなので一つのセルクラスしか用意してませんが、各ページでレイアウトが違うものにしたい場合は、別途そちら用のセルクラスを用意すればいいと思います。

InfoViewCell.xib
<img width="300" alt="InfoViewCell" src="https://github.com/ddd503/Image-Resource/raw/master/InfoViewCell.png">

```InfoViewCell.swift
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用に各種設定 & レイアウトを実装します。

```TutorialViewController.swift
@IBOutlet weak private var collectionView: UICollectionView!
```

 - 各種設定

```TutorialViewController.swift
// viewDidLoadなどのタイミングで以下を設定
collectionView.isPagingEnabled = true
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.dataSource = self
collectionView.delegate = self
// 用意したセルクラスの登録
collectionView.register(InfoViewCell.nib(),forCellWithReuseIdentifier:InfoViewCell.identifier)
```

 - データソースの反映
 
```TutorialViewController.swift
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
    }
}

```

 - レイアウトを定義 <br>今回はUICollectionViewDelegateFlowLayoutでレイアウトを作成<br> collectionViewのframeはstoryBoard側でsuperViewと同じサイズになるようにしています。
 
```TutorialViewController.swift
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 ページコントロールタップによるページシング

```TutorialViewController.swift
@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)
```

```TutorialViewController.swift
func pagingInfoList(_ newPage: Int) {
        DispatchQueue.main.async { [weak self] in
            self?.collectionView.scrollToItem(at: IndexPath(item: newPage, section: 0), at: .init(), animated: true)
        }
    }
```

- スクロールによるページシング<br>
ページング処理自体はユーザーアクションにより完結しますが、今回は「現在のページ番号」を管理するためにアクションをハンドリングしています。<br>今回はUICollectionViewDelegateがUIScrollViewDelegateに準拠しているので、以下のメソッドで拾っています。

```
optional func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
```

```TutorialViewController.swift
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