iOSで要素が下から上に向かって表示されるリストを作りたい

iOSでセルが下から上に向かって表示されるリストの作成方法について検討してみた。実装例は以下のリポジトリにある↓

https://github.com/hayason00/BottomToTopCollectionView

各方法の最後にも該当するコードへのリンクを記載している。

# 方法1 リスト全体を反転させる
一番簡単な方法。

`UICollectionView`や`UITableView`を`transform`で上下反転し、表示するセルも同様に反転させることで、下から上に向かって表示されるように見せる。
``` Swift
collectionView.transform = CGAffineTransform(scaleX: 1, y: -1)
```
``` Swift
let cell = collectionView.dequeueConfiguredReusableCell(
    using: cellRegistration,
    for: indexPath,
    item: collectionView.numberOfItems(inSection: indexPath.section) - indexPath.item - 1
)
cell.transform = CGAffineTransform(scaleX: 1, y: -1)
```

この方法は新たに下に要素が追加されると常に要素全体が上がっていく。

そのため、ユーザーがスクロールしている間に要素がどんどん追加される場合、操作中に要素の位置が変わるためユーザー操作を阻害する可能性がある。

<img width="200pt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3539478/a9fd5fd8-6676-a24b-aea7-dfff9e42717f.gif">

サンプル↓

https://github.com/hayason00/BottomToTopCollectionView/blob/main/BottomToTopCollectionView/ReversedCollectionView/ReversedCollectionViewController.swift

# 方法2 リストの上部にInsetを作る
方法１と異なり要素全体が上がらない比較的簡単な方法。

`UICollectionView`や`UITableView`の`contentSize.height`が表示領域より小さい間、`contentSize.height`がそのviewのサイズになるように上部のマージンを調整することで、下から上に向かって表示されるように見せる。

サンプルではCombineを使って`contentSize`を監視し、サイズが変化した時に`UICollectionView`の上部と親`UIView`との余白を調整している。

``` Swift
collectionView.publisher(for: \.contentSize).map(\.height).removeDuplicates().sink { [weak self] contentHeight in
    guard let collectionViewTopConstraint = self?.collectionViewTopConstraint,
          let containerViewBoundsHeight = self?.containerView.bounds.height
    else { return }
    collectionViewTopConstraint.constant = max(containerViewBoundsHeight - contentHeight, 0)
}
.store(in: &cancellables)
```
この方法だと下に要素が追加されても要素全体が上がっていくことはない。

代わりに最下部で固定したい場合は、要素が追加される度`contentOffset`の調整が必要になってくる。

<img width="200pt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3539478/ffdfebf9-5198-73c5-a361-358623ea2c43.gif">

サンプル↓

https://github.com/hayason00/BottomToTopCollectionView/blob/main/BottomToTopCollectionView/AdjustCollectionViewInset/AdjustCollectionViewInsetViewController.swift

# 方法3 UICollectionViewLayoutをカスタムする
一番面倒だが一番自由度が高い方法。

`UICollectionViewFlowLayout`か`UICollectionViewLayout`を継承したクラスを作り、自分でレイアウトを実装する。

[Customizing Collection View Layouts](https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts/customizing_collection_view_layouts)

今回は簡単に`UICollectionViewFlowLayout`を使って高さ固定を前提に作成した。
セル全ての高さ`contentSize.height`が表示領域より小さい場合、表示領域を座標系にして下部詰めで各セルを並べるようにしている。
（本来`prepare()`で諸々の計算をすべきだが、ここでは極力シンプルにするため省略している）

サンプルでは方法2と同じように下に要素が追加されても要素全体が上がらないように実装しているが、方法1と同じように作ることも可能なはずだ。

サンプル↓

https://github.com/hayason00/BottomToTopCollectionView/blob/main/BottomToTopCollectionView/CustomCollectionViewLayout/BottomToTopCollectionViewFlowLayout.swift

# 番外編 方法１の方法を使ったSwiftUIでの実装方法
方法１のView全体を反転する方法はSwiftUIでも使える。

以下のように`ScrollView`や`List`などを`rotationEffect(_:anchor:)`を使って上下反転し、内部のViewも同様に反転させる。
```Swift
struct ReversedListView: View {
    var numberOfItems: Int

    var body: some View {
        ScrollView {
            LazyVStack(alignment: .leading) {
                ForEach((0 ..< numberOfItems).reversed(), id: \.self) { index in
                    ColorCell(num: index)
                        .frame(height: 40)
                        .rotationEffect(.degrees(180))
                }
            }
        }
        .rotationEffect(.degrees(180))
    }
}
```

この方法の場合、iOS 17では方法2と同じ挙動、iOS 16以下では方法1と同じ挙動になる。
挙動がOSによって変わるのは、本番では少し使いづらいかもしれない。

サンプル↓

https://github.com/hayason00/BottomToTopCollectionView/blob/main/BottomToTopCollectionView/ReversedSwiftUIList/ReversedListView.swift

# まとめ
結果をまとめると以下のようになる。

1. リスト全体を反転する方法
    - 一番簡単
    - 新しい要素が追加されると全体が上に動く
2. リストの上部にInsetを作る方法
    - 比較的簡単
    - 要素が追加されても全体が動かない
3. UICollectionViewLayoutをカスタムする方法
    - 最も柔軟
    - 実装が複雑
4. (番外編) SwiftUIで反転する方法
    - SwiftUIなのでリスト自体の実装が楽
    - OSバージョンによって挙動が異なる

多くの場合は方法1で問題ないため、基本的には簡単な方法1を使い、挙動が仕様に合わない場合は他の案を検討するのが良さそうだ。