Search code examples
swiftswiftuitimer

SwiftUI - Increment number on long press gesture, then stop once gesture is released


I'm attempting to create a stepper where a number increments rapidly on a longpress gesture and stops when the user releases.

So far, I get the increment to work on the longPress, but when I release the timer still goes, continuing to increment the state.

What can I do to resolve this issue that when the user releases the press, the timer stops.

struct CustomFoodItemView: View {
    @State var foodName = ""
    @State var proteinAmount = 1
    @State var carbAmount = 1
    @State var fatAmount = 1
    
    @State private var timer: Timer?
    @State var isLongPressing = false
    
    var body: some View {
        VStack{
            
            VStack{
                Text("Food Name")
                TextField("", text: $foodName)
                    .multilineTextAlignment(.center)
                    .border(.white)
                    .padding(.trailing, 10)
                    .frame(width:100, height:10)
            }
            HStack{
                Text(String(proteinAmount) + "g")
                    .frame(width:50, height:50)
                
                Image(systemName: "plus.circle.fill")
                    .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 30, height: 30)
                        .foregroundColor(Color("SuccessButtonColor"))
                        .simultaneousGesture(LongPressGesture(minimumDuration: 0.2).onChanged { _ in
                                      print("long press")
                                      self.isLongPressing = true
                            if self.isLongPressing == true{
                                self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { _ in
                                    proteinAmount += 1
                                })
                            } 
                                  }
                                .onEnded { _ in
                            print("stopped") //why won't you stop
                                    self.isLongPressing = false
                                })
                       }
              }

enter image description here


Solution

  • Two things:

    1. You don't stop the timer at all, to stop it, you have to invalidate it (as Leo mentioned). You can do that with self.timer?.invalidate().
    2. The .onEndedof the LongPressGesture will be called when the LongPressGesture has been recognized by pressing the button longer than the minimumDuration time, it does not handle the event when the button will be released. So you don't want to stop the timer on .onEnded.

    Combined LongPressGesture and DragGesture

    I tried your code and an approach could be to use the LongPressGesture to start the timer and a DragGesture with a minimum distance of 0 to recognize the button release.

    This would look like:

    struct CustomFoodItemView: View {
        @State var foodName = ""
        @State var proteinAmount = 0
        @State var carbAmount = 0
        @State var fatAmount = 0
        let maxValue = 50
        let minValue = 0
        
        @State private var timer: Timer?
        
        var body: some View {
            
            // a drag gesture that recognizes the release of the button to stop the timer, set minimumDistance to 0 to ensure no dragging is required
            let releaseGesture = DragGesture(minimumDistance: 0)
                .onEnded { _ in
                    self.timer?.invalidate()
                    print("Timer stopped")
                }
            
            // a long press gesture to activate timer and start increasing the proteinAmount
            let longPressGestureIncrease = LongPressGesture(minimumDuration: 0.2)
                .onEnded { value in
                    self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { _ in
                        if proteinAmount < maxValue {
                            proteinAmount += 1
                        }
                    })
                    print("Timer started")
                }
    
    
            // a long press gesture to activate timer and start decreasing the proteinAmount
            let longPressGestureDecrease = LongPressGesture(minimumDuration: 0.2)
                .onEnded { value in
                    self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { _ in
                        if proteinAmount > minValue {
                            proteinAmount -= 1
                        }
                    })
                    print("Timer started")
                }
        
            // a combined gesture that forces the user to long press before releasing for increasing the value
            let combinedIncrease = longPressGestureIncrease.sequenced(before: releaseGesture)
        
            // a combined gesture that forces the user to long press before releasing for decreasing the value
            let combinedDecrease = longPressGestureDecrease.sequenced(before: releaseGesture)
            
            VStack{
                
                VStack{
                    Text("Food Name")
                    TextField("", text: $foodName)
                        .multilineTextAlignment(.center)
                        .border(.white)
                        .padding(.trailing, 10)
                        .frame(width:100, height:10)
                }
                HStack{
                    Image(systemName: "minus.circle.fill")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 30, height: 30)
                        .foregroundColor(.red)
                        .gesture(combinedDecrease)
    
                    Text(String(proteinAmount) + "g")
                        .frame(width:50, height:50)
                    
                    Image(systemName: "plus.circle.fill")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 30, height: 30)
                        .foregroundColor(.red)
                        .gesture(combinedIncrease)
                }
            }
        }
    }
    

    Please see the gif with the the few items from the code snippet (the number increases after starting the long press and stops when releasing the "+" button):

    Increasing number by activated timer

    Indrease and Decrease number by activating timers