How to create a View that reveals hidden buttons when slid in SwiftUI
Tags: swiftui, ios
Published: 2022-05-21 12:01:48

## Introduction

This article introduces how to implement a View in SwiftUI, in which a hidden button appears from the right when the view is slid to the left.

![The image of a view that reveals hidden buttons when slid](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o3lzvgtohwjbshrmw868.gif)

## Implementation
This can be accomplished by changing the offset of the x-axis of the View as the user drags the view.

The code executable in Xcode's Playground is as follows

```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())

```

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