【SwiftUIのみ】iOS15以下で高さが動的or静的なsheetを表示する

## iOS16以降であればAPIが存在する
iOS16以降であれば以下の記事のように自分好みな高さでsheetを表示できますよね。

https://blog.code-candy.com/swiftui_sheet_half/

ただ、iOS15以下だと使えない…
これをSwiftUIのみで解決します！

## 動作確認

![Simulator Screen Recording - iPhone 15 Pro Max - 2024-06-08 at 22.33.35.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3554113/8b20b743-3396-c454-e908-f13828d2e177.gif)

こんな感じです。
私の場合は、画面の3/1くらいでsheetを表示しました。

以下の動作でシートを閉じられるようにしています。
- シートの表示領域外をタップ
- シートを下にドラッグ


## SwftUIコード(コピペでOK)
以下が、sheetを開閉指示する画面
```swift
struct ContentView: View {
    @State private var isShowingCustomSheet = false

    var body: some View {
        GeometryReader { geometry in
            let height = geometry.size.height
            ZStack {
                Button {
                    self.isShowingCustomSheet.toggle()
                } label: {
                    Text("シートを表示する")
                }
                CustomSheet(isShowing: $isShowingCustomSheet, height: height)
            }
        }
    }
}
```

以下が、iOS15以下でも高さが動的or静的に表示できるsheet
```swift

struct CustomSheet: View {
    let height: CGFloat
    @Binding var isShowing: Bool
    @GestureState private var dragState: CGSize = .zero
    @State private var offsetY: CGFloat = 0

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .bottom) {
                if isShowing {
                    Color.black
                        .opacity(0.3)
                        .ignoresSafeArea()
                        .onTapGesture {
                            withAnimation {
                                isShowing = false
                            }
                        }

                    VStack {
                        Text("カスタムシートが表示された")
                    }
                    .frame(maxWidth: .infinity)
                    .frame(maxHeight: height / 4)
                    .background(Color.white)
                    .cornerRadius(16, corners: [.topLeft, .topRight])
                    .offset(y: offsetY + (dragState.height > 0 ? dragState.height : 0))
                    .gesture(
                        DragGesture()
                            .updating($dragState) { value, state, _ in
                                if value.translation.height > 0 {
                                    state = value.translation
                                }
                            }
                            .onEnded { value in
                                if value.translation.height > 100 {
                                    isShowing = false
                                } else {
                                    offsetY = 0
                                }
                            }
                    )
                    .transition(.opacity.combined(with: .move(edge: .bottom)))
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .ignoresSafeArea()
            .animation(.easeInOut, value: isShowing)
        }
        .edgesIgnoringSafeArea(.all)
    }
}

private struct RoundedCorner: Shape {
    var radius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners

    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        return Path(path.cgPath)
    }
}

private extension View {
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape( RoundedCorner(radius: radius, corners: corners) )
    }
}
```