Search code examples
iosswiftswiftuiapple-watchswiftui-button

SwiftUI: Long Press fills button to show progress?


I pieced together this code for a watch app and it kind of works but it's not perfect. (1) The fill does not match exactly, it starts outside the bounds of the buttons view but I can't figure out how to match the size of the button exactly. (2) I can't get the tappable area to extend to the left and right sides of the view. Even if I add .contentShape(rectangle) or .frame(maxWidth: .infinity) the only part that is tappable is the middle where the image and text are. Is there a better way to do this?

struct ContentView2: View {
    
    @State var isComplete: Bool = false
    @State var isSuccess: Bool = false
    
    var body: some View {
        VStack {
            ZStack {
                RoundedRectangle(cornerRadius: 12.0)
                    .fill(isSuccess ? Color.clear : Color.red.opacity(0.50))
                    .frame(maxWidth: isComplete ? .infinity : 0)
                    .frame(height: 55) //How to now hard code this? 
                    .frame(maxWidth: .infinity, alignment: .leading)
                Button(action: {}, label: {
                    VStack  {
                        Image(systemName: "multiply.circle.fill")
                            .font(.system(size: 24, weight: .regular))
                            .foregroundColor(.red)
                        Text("Hold to End")
                            .font(.footnote)
                    }
                    .onLongPressGesture(minimumDuration: 1.0, maximumDistance: 50) {
                        withAnimation(.easeInOut) {
                            isSuccess = true
                            //perform action
                        }
                        
                    } onPressingChanged: { isPressing in
                        if isPressing {
                            withAnimation(.easeIn(duration: 1.0)) {
                                isComplete = true
                            }
                        } else {
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                                if !isSuccess {
                                    withAnimation(.easeInOut) {
                                        isComplete = false
                                    }
                                }
                            }
                        }
                    }
                })
                .tint(.pink)
                
            }
        }
    }
}

#Preview {
    ContentView2()
}

Solution

  • (1) The fill does not match exactly, it starts outside the bounds of the buttons view but I can't figure out how to match the size of the button exactly.

    Make the rounded rectangle a background of the button's label, then it will automatically be given the same size:

    Button(action: {}, label: {
        VStack  {
            ...
        }
        .background {
            RoundedRectangle(cornerRadius: 12.0)
                .fill(isSuccess ? Color.clear : Color.red.opacity(0.50))
                .frame(maxWidth: isComplete ? .infinity : 0)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    

    (2) I can't get the tappable area to extend to the left and right sides of the view.

    You're probably adding the frame modifier to the wrong place. The Button itself isn't actually doing anything here, and removing that might make the picture clearer for you, but you want the label of the button to be the thing that is max width'd, and this has to be before the background if you want the background to fill in the entire width, or after it if you want the background to match the width of the visible contents:

    Button(action: {}, label: {
        VStack  {
            ...
        }
        .frame(maxWidth: .infinity)
        .background {
            ...
        }
        .contentShape(Rectangle())
        .onLongPressGesture ....