SwiftUI: how to handle BOTH tap & long press of button?

前端 未结 8 1203
刺人心
刺人心 2020-12-15 06:59

I have a button in SwiftUI and I would like to be able to have a different action for \"tap button\" (normal click/tap) and \"long press\".

Is that possible in Swift

相关标签:
8条回答
  • 2020-12-15 07:59

    Thought I'd post back on this, in case anyone else is struggling. Strange that Apple's default behaviour works on most controls but not buttons. In my case I wanted to keep button effects while supporting long press.

    An approach that works without too much complexity is to ignore the default button action and create a simultaneous gesture that handles both normal and long clicks.

    In your view you can apply a custom long press modifier like this:

    var body: some View {
    
            // Apply the modifier
            Button(action: self.onReloadDefaultAction) {
                Text("Reload")
            }
                .modifier(LongPressModifier(
                    isDisabled: self.sessionButtonsDisabled,
                    completionHandler: self.onReloadPressed))
        }
    
        // Ignore the default click
        private func onReloadDefaultAction() {
        }
    
        // Handle the simultaneous gesture
        private func onReloadPressed(isLongPress: Bool) {
    
            // Do the work here
        }
    

    My long press modifier implementation looked like this and uses the drag gesture that I found from another post. Not very intuitive but it works reliably, though of course I would prefer not to have to code this plumbing myself.

    struct LongPressModifier: ViewModifier {
    
        // Mutable state
        @State private var startTime: Date?
    
        // Properties
        private let isDisabled: Bool
        private let longPressSeconds: Double
        private let completionHandler: (Bool) -> Void
    
        // Initialise long press behaviour to 2 seconds
        init(isDisabled: Bool, completionHandler: @escaping (Bool) -> Void) {
    
            self.isDisabled = isDisabled
            self.longPressSeconds = 2.0
            self.completionHandler = completionHandler
        }
    
        // Capture the start and end times
        func body(content: Content) -> some View {
    
            content.simultaneousGesture(DragGesture(minimumDistance: 0)
                .onChanged { _ in
    
                    if self.isDisabled {
                        return
                    }
    
                    // Record the start time at the time we are clicked
                    if self.startTime == nil {
                        self.startTime = Date()
                    }
                }
                .onEnded { _ in
    
                    if self.isDisabled {
                        return
                    }
    
                    // Measure the time elapsed and reset
                    let endTime = Date()
                    let interval = self.startTime!.distance(to: endTime)
                    self.startTime = nil
    
                    // Return a boolean indicating whether a normal or long press
                    let isLongPress = !interval.isLess(than: self.longPressSeconds)
                    self.completionHandler(isLongPress)
                })
        }
    }
    
    0 讨论(0)
  • 2020-12-15 08:00

    I had to do this for an app I am building, so just wanted to share. Refer code at the bottom, it is relatively self explanatory and sticks within the main elements of SwiftUI.

    The main differences between this answer and the ones above is that this allows for updating the button's background color depending on state and also covers the use case of wanting the action of the long press to occur once the finger is lifted and not when the time threshold is passed.

    As noted by others, I was unable to directly apply gestures to the Button and had to apply them to the Text View inside it. This has the unfortunate side-effect of reducing the 'hitbox' of the button, if I pressed near the edges of the button, the gesture would not fire. Accordingly I removed the Button and focused on manipulating my Text View object directly (this can be replaced with Image View, or other views (but not Button!)).

    The below code sets up three gestures:

    1. A LongPressGesture that fires immediately and reflects the 'tap' gesture in your question (I haven't tested but this may be able to replaced with the TapGesture)

    2. Another LongPressGesture that has a minimum duration of 0.25 and reflect the 'long press' gesture in your question

    3. A drag gesture with minimum distance of 0 to allow us to do events at the end of our fingers lifting from the button and not automatically at 0.25 seconds (you can remove this if this is not your use case). You can read more about this here: How do you detect a SwiftUI touchDown event with no movement or duration?

    We sequence the gestures as follows: Use 'Exclusively' to combine the "Long Press" (i.e. 2 & 3 above combined) and Tap (first gesture above), and if the 0.25 second threshold for "Long Press" is not reached, the tap gesture is executed. The "Long Press" itself is a sequence of our long press gesture and our drag gesture so that the action is only performed once our finger is lifted up.

    I also added code in the below for updating the button's colours depending on the state. One small thing to note is that I had to add code on the button's colour into the onEnded parts of the long press and drag gesture because the minuscule processing time would unfortunately result in the button switching back to darkButton colour between the longPressGesture and the DragGesture (which should not happen theoretically, unless I have a bug somewhere!).

    You can read more here about Gestures: https://developer.apple.com/documentation/swiftui/gestures/composing_swiftui_gestures

    If you modify the below and pay attention to Apple's notes on Gestures (also this answer was useful reading: How to fire event handler when the user STOPS a Long Press Gesture in SwiftUI?) you should be able to set up complex customised button interactions. Use the gestures as building blocks and combine them to remove any deficiency within individual gestures (e.g. longPressGesture does not have an option to do the events at its end and not when the condition is reached).

    P.S. I have a global environment object 'dataRouter' (which is unrelated to the question, and just how I choose to share parameters across my swift views), which you can safely edit out.

    struct AdvanceButton: View {
    
    @EnvironmentObject var dataRouter: DataRouter
    
    @State var width: CGFloat
    @State var height: CGFloat
    @State var bgColor: Color
    
    @GestureState var longPress = false
    @GestureState var longDrag = false
    
    var body: some View {
    
        let longPressGestureDelay = DragGesture(minimumDistance: 0)
            .updating($longDrag) { currentstate, gestureState, transaction in
                    gestureState = true
            }
        .onEnded { value in
            print(value.translation) // We can use value.translation to see how far away our finger moved and accordingly cancel the action (code not shown here)
            print("long press action goes here")
            self.bgColor = self.dataRouter.darkButton
        }
    
        let shortPressGesture = LongPressGesture(minimumDuration: 0)
        .onEnded { _ in
            print("short press goes here")
        }
    
        let longTapGesture = LongPressGesture(minimumDuration: 0.25)
            .updating($longPress) { currentstate, gestureState, transaction in
                gestureState = true
        }
        .onEnded { _ in
            self.bgColor = self.dataRouter.lightButton
        }
    
        let tapBeforeLongGestures = longTapGesture.sequenced(before:longPressGestureDelay).exclusively(before: shortPressGesture)
    
        return
            Text("9")
                .font(self.dataRouter.fontStyle)
                .foregroundColor(self.dataRouter.darkButtonText)
                .frame(width: width, height: height)
                .background(self.longPress ? self.dataRouter.lightButton : (self.longDrag ? self.dataRouter.brightButton : self.bgColor))
                .cornerRadius(15)
                .gesture(tapBeforeLongGestures)
    
        }
    
    }
    
    0 讨论(0)
提交回复
热议问题