【Swift】 CollectionView入門

CollectionViewを活用すると、複数のビューをまとめて管理することができます。特に、同じような見た目のビューをいくつも表示させたり、オセロ盤のようなマス目の表現にCollectionViewは向いています。

今回は以下のようなCollectionViewを作成してみます。
（環境：Xcode12.4 　Swift5.3）

<img width="300" alt="CollectionView_03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/a6c1b791-1b05-c8e3-e032-61f25c7a3a3f.png">


##手順

CollectionView作成の手順は次のようになります。

1. StoryBoard(ViewController)にUICollectionViewを配置
- CollectionViewCellの中にUIlabelを配置 & 関連付け
- ViewController.swiftファイルに変数を用意 & 関連付け
- ViewControllerにプロトコルを追加 (UICollectionViewDataSource, UICollectionViewDelegate)
- DataSourceとDelegateをViewController(Self)に指定 
- メソッドを追加
- UICollectionViewLayoutクラスを継承したCollectionViewLayoutクラスを作成 & Layoutに指定
- UICollectionViewCellクラスを継承したCollectionViewCellクラスを作成 & Custom Classに指定
- 各セルに表示させる内容(text)をメソッドに記述


##1. StoryBoard(ViewController)にUICollectionViewを配置
StoryBoardのUIViewControllerにUICollectionViewを配置しましょう。今回配置するUIViewControllerには ViewControllerクラスが設定されているとして説明していきます。ちなみに、CollectionViewの位置はAutoLayoutで設定するとうまく表示されないため、後ほどコードから設定していきます。
<img width="600" alt="1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/e755e2f9-91f7-9965-222a-1bd2f2ee4a51.png">

##2. CollectionViewCellの中にUIlabelを配置 & 関連付け
CollectionViewを配置すると、その中に自動でCollectionViewCellも1つだけ配置されます。そのCellの中にLabelを配置していきます。ここではAutoLayoutが利用できるので、Cellの枠との幅を0に設定しておきます。

<img width="600" alt="2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/3499a91d-c1e3-996d-07b8-6acbb179b947.png">

続いてCollectionViewCellのコネクションインスペクタを開いて、Outletsのlabelと先ほど配置したLabelとを関連付けします。これでCollectionViewCellのインスタンスから.labelとアクセスできるようになります。

<img width="600" alt="3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/30e906ef-3168-d0d1-6a1e-6d1a5817a2fc.png">


##3. ViewController.swiftファイルに変数を用意 & 関連付け
UIViewControllerのインスタンスを作成します。加えてStoryBoard上のUIViewControllerと関連付けしておきましょう。
```swift:ViewController
    @IBOutlet var collectionView: UICollectionView!
```

##4. ViewControllerにプロトコルを追加
ViewControllerに以下のプロトコルを追加します。

- UICollectionViewDataSource
- UICollectionViewDelegate

```swift:ViewController
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
    /*処理*/
}
```

##5. DataSourceとDelegateをViewController(Self)に指定 
ViewControllerの`viewDidLoad`メソッドの中で以下の記述を追加します。ついでにCollectionViewの配置場所の指定もしておきましょう。ここでは左右に少し余白を設け、画面中央に配置するようにしています。

```swift:ViewController
    override func viewDidLoad() {
        super.viewDidLoad()
        self.collectionView.bounds.size.width = self.view.bounds.size.width-10
        self.collectionView.bounds.size.height = self.view.bounds.size.width-10
        self.collectionView.center = self.view.center
        
        collectionView.dataSource = self
        collectionView.delegate = self
    }
```

##6. メソッドを追加
CollectionViewの処理に関わる以下のメソッドを追加します。

```swift:CollectionView
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        //これは同じ設定のCollectionViewの数(Section数)を指定するメソッド。
        //今回は1を返しておきましょう
        return 1
      }
      
    //UICollectionViewDataSourceを読み込んだ時点で赤い警告が表示されるのでfixボタンをクリックすると自動で挿入される
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        //これはcollectionViewの各セクションについてアイテム数(Cellの数)を指定するメソッド
        //今回は64を返しておきましょう
        return 64
  }
      
     //UICollectionViewDataSourceを読み込んだ時点で赤い警告が表示されるのでfixボタンをクリックすると自動で挿入される     
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        //これはcollectionView内の個々のcellについての処理をおこなうメソッド
        //あとで処理を記述します
      }
      
      func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        //これはcell選択(クリック)時に呼ばれるメソッド
      }
```


##7. UICollectionViewLayoutクラスを継承したCollectionViewLayoutクラスを作成 & Layoutに指定

続いて、個々のCellのサイズや配置場所を設定するクラスを作成していきます。
次のコードでは、64個のCellを左上から右下へと順に配置しています。

```swift:CollectionViewLayout
import UIKit
 
class CollectionViewLayout: UICollectionViewLayout{
    //行数
    let columns = 8
    //レイアウト
    private var layoutData = [UICollectionViewLayoutAttributes]()
    
    //レイアウトの準備
    override func prepare() {
        super.prepare()
        self.layoutData.removeAll()
        let allWidth  = collectionView!.bounds.width
        let columnWidth = allWidth / CGFloat(self.columns)
        let columnHeight = columnWidth
        var x:CGFloat = 0
        var y:CGFloat = 0

        for count in 0 ... collectionView!.numberOfItems(inSection: 0){
            let indexPath = NSIndexPath(item: count, section: 0)
            //レイアウトの配列に位置とサイズを登録する。
            let frame = CGRect(x: x, y: y, width: columnWidth, height: columnHeight)
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
            attributes.frame = frame
            layoutData.append(attributes)
            
            //これはindexPath[1]が左上が0,右下が63となるように配置している
            if ((count + 1) % columns != 0){
                x += columnWidth
            }else{
                x = 0
                y += columnHeight
            }
        }
    }
    
    //レイアウトを返す
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return layoutData
    }
}
```

`CollectionViewLayout`クラスが作成できたら、StoryboardにてCollectionViewのLayoutに指定します。
<img width="600" alt="4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/3cc66dea-1357-9856-d56d-fd7f23f91a35.png">


##8. UICollectionViewCellクラスを継承したCollectionViewCellクラスを作成 & Custom Classに指定

全てのCellにわたるレイアウトを設定するクラスを作成します。
次のコードではセルの枠線の太さと色を設定しています

```swift:CollectionViewCell
import UIKit

class CollectionViewCell: UICollectionViewCell {
    
    
    @IBOutlet var label: UILabel!
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        //cellの枠の太さ
        self.layer.borderWidth = 1.4
        //cellの枠の色
        self.layer.borderColor = #colorLiteral(red: 0.9086276889, green: 0.2920762599, blue: 0.2549881935, alpha: 1)
}
```
色をコードで代入する場合には、以下のように予測変換を用いて`Color Literal`と入力することで直感的に色を指定できます。
<img width="600" alt="5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/d07a4ae9-4f87-76b9-00c3-a703ecf20e97.png">
<img width="600" alt="6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/d1fe138a-9a18-3ef4-45ac-8dc3182671e2.png">

またStoryboardにてCollectionViewCellのCustom Classに今作成したクラスを指定します。
<img width="600" alt="6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/1072688f-9ea2-15d7-7898-7410128be132.png">

##9. 各セルに表示させる内容(text)をメソッドに記述 
コードから各セルにアクセスできるよう、StoryboardにてCollectionViewCellのCollectionReusableViewのIdentifierに`CollectionViewCell`と設定しておきます。(先ほど作成したクラス名と同じなのはたまたまです)

続いてViewControllerの`collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell`メソッドに処理を追記していきます。ここでは、各セルの番号(numberOfItems)を表示するようにしています。引数として受け取る`indexPath`は2次元配列で、`indexPath[0]`にSection、`indexPath[1]`にSection内のCell番号(`collectionView!.numberOfItems(inSection: 0)`)が格納されています。


```swift:ViewController
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
    
        cell.label.text = String(indexPath[1])
    
        return cell
    }
```


##実行結果
実行結果は冒頭に示したように次のようになります。

<img width="300" alt="CollectionView_03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/a6c1b791-1b05-c8e3-e032-61f25c7a3a3f.png">

##応用

CollectionViewLayoutと、ViewController内の各セルの処理をするメソッドの記述を変更すると、例えば次のような表現もできるようになります。

<img width="300" alt="CollectionView_04" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498332/7118713c-40f7-78a2-64e3-119c65489b4b.png">



###参考

この記事は以下の情報を参考にして執筆しました。

- [CollectionViewの基礎]
(https://qiita.com/azuma317/items/bf1a687f912159297cb1)
- [螺旋のCollectionViewを作ってみる]
(https://qiita.com/Sho-heikun/items/4b9c85a04957c5b66e7d)