iosswiftswiftuimenupinterest

SwiftUI: Using LongPressGesture to display a Pinterest like context menu


I am trying create a context menu similar to Pinterest's context menu in their iOS app. Long pressing a post reveals a four button view, which while the user continues the longpress, is then able to drag and select the other buttons. Letting go of the long press will either select whichever button you are currently selecting or dismiss the menu altogether if you don't have anything selected. Please see an example of this below:

pinterest context menu

So far, I've tried something similar to Apple's documentation here: https://developer.apple.com/documentation/swiftui/longpressgesture

But it seems that the gesture finishes as soon as it hits the minimumDuration defined in the gesture. I'd like the gesture to continue for as long as the user is holding, and end as soon as they let go.

Additionally, I am in the weeds when it comes to the dragging and selecting the other buttons. Here is my approach so far:

struct Example: View {

@GestureState var isDetectingLongPress = false
@State var completedLongPress = false

var longPress: some Gesture {
    LongPressGesture(minimumDuration: 3)
        .updating($isDetectingLongPress) { currentState, gestureState,
                transaction in
            gestureState = currentState
            transaction.animation = Animation.easeIn(duration: 2.0)
        }
        .onEnded { finished in
            self.completedLongPress = finished
        }
}

var body: some View {
    
    HStack {
        
        Spacer()
        ZStack {
            // Three button array to fan out when main button is being held
            Button(action: {
                // ToDo
            }) {
                Image(systemName: "circle.fill")
                    .frame(width: 70, height: 70)
                    .foregroundColor(.red)
            }
            .offset(x: self.isDetectingLongPress ? -90 : 0, y: self.isDetectingLongPress ? -90 : 0)
            Button(action: {
                // ToDo
            }) {
                Image(systemName: "circle.fill")
                    .frame(width: 70, height: 70)
                    .foregroundColor(.green)
            }
            .offset(x: 0, y: self.isDetectingLongPress ? -120 : 0)
            Button(action: {
                // ToDo
            }) {
                Image(systemName: "circle.fill")
                    .frame(width: 70, height: 70)
                    .foregroundColor(.blue)
            }
            .offset(x: self.isDetectingLongPress ? 90 : 0, y: self.isDetectingLongPress ? -90 : 0)
            
            // Main button
            Image(systemName: "largecircle.fill.circle")
                .gesture(longPress)
            
        }
        Spacer()
    }

}

Solution

  • Update: I found a solution that works, but it's using UIKit as I don't believe SwiftUI provides a way to do this natively. I followed the guidance from the answer to a similar question found here: https://stackoverflow.com/a/31591162/862561

    It was however written in ObjC, so I translated roughly to Swift. For those who are curious, here is a simplified version of the process:

    class UIControlsView: UIView {
    
    let createButton = CreateButtonView()
    let secondButton = ButtonView(color: .red)
    var currentDraggedButton: ButtonView!
    
    required init?(coder: NSCoder) {
        fatalError("-")
    }
    
    init() {
        super.init(frame: .zero)
        self.backgroundColor = .green
        
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(self.longPress))
        createButton.addGestureRecognizer(longPress)
        createButton.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
        createButton.center = CGPoint(x: UIScreen.main.bounds.size.width / 2, y: 40)
        
        secondButton.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
        secondButton.center = CGPoint(x: UIScreen.main.bounds.size.width / 2, y: 40)
    
        self.addSubview(secondButton)
        self.addSubview(createButton)
    }
    
    @objc func longPress(sender: UILongPressGestureRecognizer) {
        if sender.state == .began {
            print("Started Long Press")
            secondButton.center.x = secondButton.center.x + 90
        }
        
        if sender.state == .changed {
            let location = sender.location(in: self)
            guard let superViewLocation = self.superview?.convert(location, from: self) else {
                return
            }
    
            guard let view = self.superview?.hitTest(superViewLocation, with: nil) else {
                return
            }
    
            if view.isKind(of: ButtonView.self) {
                let touchedButton = view as! ButtonView
                
                if self.currentDraggedButton != touchedButton {
                    if self.currentDraggedButton != nil {
                        self.currentDraggedButton.untouchedUp()
                    }
                }
                
                self.currentDraggedButton = touchedButton
                touchedButton.isTouchedUp()
            } else {
                if self.currentDraggedButton != nil {
                    print("Unsetting currentDraggedButton")
                    self.currentDraggedButton.untouchedUp()
                }
            }
        }
        
        if sender.state == .ended {
            print("Long Press Ended")
            let location = sender.location(in: self)
            guard let superViewLocation = self.superview?.convert(location, from: self) else {
                return
            }
    
            guard let view = self.superview?.hitTest(superViewLocation, with: nil) else {
                return
            }
            
            if view.isKind(of: ButtonView.self) {
                let touchedButton = view as! ButtonView
                
                touchedButton.untouchedUp()
                touchedButton.tap()
                self.currentDraggedButton = nil
            }
    
            secondButton.center.x = secondButton.center.x - 90
        }
        
    }