スクロールで伸縮するヘッダをSwiftUIで実装する

# はじめに
この記事はand factory.inc Advent Calendar 2022 11日目の記事です。

and factory iOSエンジニアのy-okuderaです！
SwiftUIで、以下のように引っ張ると伸縮するヘッダを実装しましたので、まとめてみます。

![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/d9746784-4a1d-ef40-5bd2-27bf14b61960.gif)

## StatusBarまで画像を描画する
まずは、画像をStatusBarまで描画してみます。
ScrollViewに `.edgesIgnoringSafeArea(.top)` を指定して、StatusBarまで描画されるようにします。

ScrollViewのサイズ・位置を取得するために、GeometryReaderを使用して、その中にImageを実装します。
Imageのサイズは、GeometryReaderと同じに設定して、GeometryReaderには任意の高さを設定します。
GeometryReaderに指定する高さは、使用する画像のサイズによって変更する必要があるので、注意が必要です:point_up_2:

```swift: ContentView.swift
import SwiftUI

struct ContentView: View {

    var body: some View {
        ScrollView {
            GeometryReader { geometryProxy in
                Image("header") // Assetsに追加した画像名
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: geometryProxy.size.width, height: geometryProxy.size.height)
            }
            .frame(height: 350)
        }
        .edgesIgnoringSafeArea(.top)
    }
}
```

画面の上端から画像を描画することができました。この時点では、引っ張っても伸縮されません。

![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/8e74d5b6-b7c4-bcb4-4208-f39ce05151cd.gif)

## ヘッダを伸縮させる

### スクロール位置が上端より上かどうか判定する
ヘッダを伸縮させるために、スクロール位置が初期位置より上かどうか判定できるようにします。
GeometryReaderのminYを使用します。GeometryReaderのminYは初期位置だと0で、上にスクロールするとプラス、下にスクロールするとマイナスの値になります。

|スクロール位置|minYの値|
|:--:|:--:|
|初期位置|GeometryReaderのminY == 0|
|初期位置より上|GeometryReaderのminY > 0|
|初期位置より下|GeometryReaderのminY < 0|


画面全体を基準としてScrollViewの上端の位置を取得したいので、globalの座標空間を指定してframeを取得し、そのminYを使用します。

```swift
/// スクロール位置が初期位置より上かどうか
private func isAboveTop(geometryProxy: GeometryProxy) -> Bool {
    return geometryProxy.frame(in: .global).minY > 0
}
```

### スクロール位置が初期位置より上のとき、画像を伸縮させる
条件判定ができるようになったので、画像を伸縮させます。
スクロール位置が初期位置より上のとき、初期位置との差分だけ画像の高さを高くして、その分だけ画像の位置を調整します。

```swift:
import SwiftUI

struct ContentView: View {

    private func isAboveTop(geometryProxy: GeometryProxy) -> Bool {
        return geometryProxy.frame(in: .global).minY > 0
    }

    var body: some View {
        ScrollView {
            GeometryReader { geometryProxy in
                ZStack {
                    if isAboveTop(geometryProxy: geometryProxy) {
                        Image("header")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: geometryProxy.size.width, height: geometryProxy.size.height + geometryProxy.frame(in: .global).minY) // 初期位置からの差分だけ高さを追加
                            .clipped()
                            .offset(y: -geometryProxy.frame(in: .global).minY) // 高さを追加した分位置を調整
                    } else {
                        Image("header")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: geometryProxy.size.width, height: geometryProxy.size.height)
                    }
                }
            }
            .frame(height: 350)
        }
        .edgesIgnoringSafeArea(.top)
    }
}
```

スクロールに応じて、画像を伸縮できました。
![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/15c911c1-25d2-f2ec-f72e-7ed714840564.gif)

# おまけ
ヘッダだけでは寂しいので、クリスマスっぽいフリー素材をたくさん集めてきて、Pinterest風に並べてみました。

![result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/91077208-c4bf-e878-dac2-1645a41ffaf0.gif)

SwiftUIMasonryというPackageを使用すると、簡単に実装できますので、詳細は割愛します:pray:

https://github.com/ciaranrobrien/SwiftUIMasonry

# おわりに
今回は、SwiftUIで、スクロールでストレッチするヘッダを実装してみました。
サンプルは以下のリポジトリにpushしてあります。

https://github.com/y-okudera/StretchyHeader

最後まで見ていただいてありがとうございました。
明日の[Advent Calendar](https://qiita.com/advent-calendar/2022/andfactory)の記事もお楽しみに:santa: