スライドすると隠れたボタンが現れる View の作成方法 in SwiftUI

# 概要
iOS において、左にスライドすると、右から隠れたボタンが現れる View の Swift UI での実装方法について紹介します。

![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1322870/93196eda-edc7-8131-3e04-cbe213e6b6a8.gif)


# 実装方法

ユーザがボタンをドラッグするのに合わせ、 View の x 軸のオフセットを変更することで、このような View を実現することができます。

Xcode の Playground で実行可能なコードは以下のとおりです。

```swift
import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    var body: some View {
        VStack(spacing: 0) {
            SlideButtons()
                .frame(width: 300, height: 40)
            SlideButtons()
                .frame(width: 300, height: 40)
            SlideButtons()
                .frame(width: 300, height: 40)
        }
    }
}

struct SlideButtons: View {
    // The current x offset of the view.
    @State private var xOffset = CGFloat.zero
    
    // The default x offset when a drag ends.
    @State private var defaultXOffset = CGFloat.zero
    
    // Initialize it on `.onAppear()`.
    @State private var hiddenButtonWidth: CGFloat = 0
    
    @State private var hiddenButtonMaxWidth: CGFloat = 50
    
    // Ratio of a hidden button width to the entire View.
    private let buttonSizeRatio = 6
    
    private var dragGesture: some Gesture {
        DragGesture(minimumDistance: 30, coordinateSpace: .local)
            .onChanged { value in
                self.xOffset += value.translation.width
                if self.hiddenButtonMaxWidth * -2 <= self.xOffset && self.xOffset <= 0 {
                    self.hiddenButtonWidth = min(self.xOffset / -2, self.hiddenButtonMaxWidth)
                }
            }
            .onEnded { value in
                withAnimation() {
                    if self.areHiddenButtonsShowed() {
                        if (self.xOffset - self.defaultXOffset) > self.getMinDistanceToChangeDefaultXOffset() {
                            self.hideButtons()
                        } else {
                            // Return to the current default position.
                            self.xOffset = self.defaultXOffset
                            // Reset the button size
                            self.hiddenButtonWidth = self.hiddenButtonMaxWidth
                        }
                        
                    } else {
                        if (self.defaultXOffset - self.xOffset) > self.getMinDistanceToChangeDefaultXOffset() {
                            self.showHiddenButtons()
                        } else {
                            // Return to the current default position.
                            self.xOffset = self.defaultXOffset
                            // Reset the button size
                            self.hiddenButtonWidth = 0
                        }
                    }
                }
            }
    }
    
    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {
                // Main button
                Rectangle()
                    .foregroundColor(.black)
                    .overlay {
                        HStack {
                            Text("Read a book")
                                .foregroundColor(.white)
                                .padding()
                            Spacer()
                        }
                    }
                    .frame(width: geometry.size.width,
                           height: geometry.size.height)
                    .gesture(
                        self.dragGesture
                    )
                    .onTapGesture {
                        withAnimation() {
                            if self.areHiddenButtonsShowed() {
                                self.hideButtons()
                            } else {
                                self.showHiddenButtons()
                            }
                        }
                    }
                
                // 1st hidden button
                Rectangle()
                    .frame(width: self.hiddenButtonWidth, height: geometry.size.height)
                    .foregroundColor(Color.yellow)
                    .overlay {
                        Text("Skip")
                            .foregroundColor(.white)
                    }
                
                // 2nd hidden button
                Rectangle()
                    .frame(width: self.hiddenButtonWidth, height: geometry.size.height)
                    .foregroundColor(Color.red)
                    .overlay {
                        Text("Done")
                            .foregroundColor(.white)
                    }
            }
            .onAppear() {
                self.hiddenButtonWidth = 0
                self.hiddenButtonMaxWidth = geometry.size.width / CGFloat(self.buttonSizeRatio)
                self.hideButtons()
            }
            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .leading)
            .offset(x: self.xOffset)
        }
    }
    
    private func areHiddenButtonsShowed() -> Bool {
        return self.defaultXOffset == self.getXOffsetToShowHiddenButtons()
    }
    
    private func hideButtons() {
        self.hiddenButtonWidth = 0
        self.xOffset = CGFloat.zero
        self.defaultXOffset = .zero
    }
    
    private func getMinDistanceToChangeDefaultXOffset() -> CGFloat {
        return self.hiddenButtonMaxWidth / 2
    }
    
    private func getXOffsetToShowHiddenButtons() -> CGFloat {
        return (self.hiddenButtonMaxWidth * 2) * -1
    }
    
    private func showHiddenButtons() {
        self.hiddenButtonWidth = self.hiddenButtonMaxWidth
        self.xOffset = self.getXOffsetToShowHiddenButtons()
        self.defaultXOffset = self.getXOffsetToShowHiddenButtons()
    }
}

PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())


```

# 参考
* https://swiftui.i-app-tec.com/ios/draggesture.html
* https://d1v1b.com/swiftui/twitter_home_menu
* https://stackoverflow.com/questions/57700396/adding-a-drag-gesture-in-swiftui-to-a-view-inside-a-scrollview-blocks-the-scroll