SwiftUI で Slack の様なサイドバーを作る

こんな感じです。
![side-gif2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178570/46f94cd9-25eb-340c-0fac-575747c5212f.gif)

```swift
import SwiftUI

struct ContentView: View {

    @State private var offset = CGFloat.zero
    @State private var initialOffset = CGFloat.zero

    @State private var isCoverPresent = false
    @State private var isSidebarPresent = false

    @State private var displayWidth = CGFloat.zero
    private var sidebarWidth:CGFloat { displayWidth * 0.85 }
    
    @State private var animation = Animation.linear(duration: 0.1)

    func openSidebar() {
        offset = .zero
        initialOffset = offset
        isSidebarPresent = true
    }

    func closeSidebar() {
        offset = -sidebarWidth
        initialOffset = offset
        isSidebarPresent = false
    }

    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {

                SidebarView()
                    .frame(width: sidebarWidth)

                ZStack {
                    Text("右にスワイプでサイドバーを開く")
                        .frame(width: displayWidth, height: geometry.size.height) // VStack の高さを画面いっぱいにして onTapGesture するようにする
                        .background(Color.orange)

                    if isCoverPresent {
                        Color.black
                            .opacity(Double((1 + self.offset / sidebarWidth) * 0.6))
                            .onTapGesture { withAnimation(animation) { closeSidebar() } }
                    }
                }
            }
            .offset(x: offset)
            .onAppear {
                displayWidth = geometry.size.width
                closeSidebar()
            }
            .gesture(DragGesture(minimumDistance: 1) // minumumDistance: 0 にすると、サイドバーやメイン画面の Button が反応しなくなるので 1 を設定
                .onChanged{ value in
                    // サイドバーが右に行きすぎていないなら
                    if (self.offset <= CGFloat.zero) {
                        offset = initialOffset + value.translation.width
                        isCoverPresent = true
                    }
                }
                .onEnded { value in
                    withAnimation(animation) {
                        if isSidebarPresent {
                            // 左にスワイプしたら
                            if value.location.x < value.startLocation.x - displayWidth * 0.1 {
                                closeSidebar()
                            } else {
                                openSidebar()
                            }
                        } else {
                            // 右にスワイプしたら
                            if value.location.x > value.startLocation.x + displayWidth * 0.2 {
                                openSidebar()
                            } else {
                                closeSidebar()
                            }
                        }
                    }
                }
            )
        }
    }
}

fileprivate struct SidebarView: View {
    var body: some View {
        Text("サイドバー")
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
            .background(Color.blue)
            .foregroundColor(.white)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```