Legnus
Legnus

Reputation: 159

How to track button selection after long press?

I am trying to imitate the long press of the keyboard suggesting a letter for UIButton control. What I am trying to do is long press on UIButton, after keep pressing on the button 3 new buttons shows and select one of these 3 new buttons. just like the Keyboard letter suggestion. enter image description here

How can I do this? any idea? Thank you

Upvotes: 0

Views: 540

Answers (3)

Michael Konz
Michael Konz

Reputation: 543

This is quite old, but as I had the same problem I will present my solution for it.

I have my keyboard laid out and created by class Keyboard, derived from UIView Each of the letter buttons is of class LetterButton, derived from UIButton. Keyboard implements a protocol which handles KeyPressed events from the buttons.

For each button of the main keyboard a UILongPressGestureRecognizer is applied:

let longTouchRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onButtonLongPressed))
longTouchRecognizer.cancelsTouchesInView = false
button.addGestureRecognizer(longTouchRecognizer)
button.delegate = self

with

@objc func onButtonLongPressed (_ sender: UIGestureRecognizer)
{
    if (sender.state == .began)
    {
        guard let tag = sender.view?.tag else { return }
        createPopupView(button: buttons[tag])
    }
}

It is essential to set cancelsTouchesInView to false, else we wouldn't receive any further events !

On a longtouch on a button a popupview is created with one or more buttons above the button touched. We can swipe to these buttons directly from the button touched.

Implementation of LetterButton class:

class LetterButton : UIButton
{
  var delegate : LetterButtonDelegate?
  var isInside = false

This is called from Keyboard class:

  func setIsInside(val: Bool)
  {
    if (val)
    {
        if (!isInside)
        {
            setBackgroundColor(UIColor.lightGray, for: .normal)
        }
    }
    else
    {
        if (!isInside)
        {
            setBackgroundColor(UIColor.white, for: .normal)
       }
    }
    isInside = val
}

Actually I only need the button here for the Keyboard class

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
    if let touch = touches.first
    {
        let point = touch.location(in: self)
        delegate?.onBegan(button: self, point: point)
    }

    super.touchesBegan(touches, with: event)
}

As long as we move the touch outside of the button, the movement information will be sent to the Keyboard class:

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
{
    if let touch = touches.first
    {
        let point = touch.location(in: self)
        if !bounds.contains(point)
        {
            delegate?.onMoved(point: convert(point, to: superview))
            return
        }
    }

    super.touchesMoved(touches, with: event)
}

This is where the letter of the button is actually processed

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
{
    if let touch = touches.first
    {
        let point = touch.location(in: self)
        delegate?.onEnded(point: convert(point, to: superview))
    }

    super.touchesEnded(touches, with: event)
}
}

As we see, the LetterButton class establishes the following protocol which need to be implemented by the Keyboard class:

protocol LetterButtonDelegate
{
    func onBegan(button: LetterButton, point: CGPoint)
    func onMoved(point: CGPoint)
    func onEnded(point: CGPoint)
}

The implementation of the protocol within Keyboard class is as follows:

The button which got initially touched is stored here

func onBegan(button: LetterButton, point: CGPoint)
{
    buttonPressed = button
}

Processes background color change for buttons we swipe over

func onMoved(point: CGPoint)
{
    let _ = findPopupButton(point: point)
}

Processing of the touch ending

func onEnded(point: CGPoint)
{
    // Check if touch ended on a popup button
    if let button = findPopupButton(point: point)
    {
        // yes, let the keyboard process the key
        delegate?.KeyPressed(key: button.title(for: .normal)!)
        button.setIsInside(val: false)

        // remove popupbuttons
        popupView?.removeFromSuperview()
        popupView = nil
    }
    else
    {
        // remove popup buttons if touch ended anywhere else
        if popupView != nil
        {
            popupView!.removeFromSuperview()
            popupView = nil

        }

        // buttons is an array of all normal keyboard buttons
        // we use it to check if the button, where the touch ended is the same where the touch began
        for button in buttons
        {
            if (button.frame.contains(point))
            {
                if (button.button.tag == buttonPressed?.tag)
                {
                    // Still on same button, process the key
                    delegate?.KeyPressed(key: button.button.title(for: .normal)!)              break
                }
            }
        }


    }
}


// Let's see if we are moving within the bounds of a popup button
func findPopupButton (point: CGPoint) -> LetterButton?
{
    var result : LetterButton? = nil

    if (popupView != nil)
    {
        if (popupView!.frame.contains(point))
        {
            for sub in popupView!.subviews
            {
                if (sub.isKind(of: LetterButton.self))
                {
                    let button = sub as! LetterButton
                    let frame = popupView!.convert(button.frame, to: self)

                    if (frame.contains(point))
                    {
                        button.setIsInside(val: true)
                        result = button
                    }
                    else
                    {
                        button.setIsInside(val: false)
                    }
                }
            }
        }
    }

    return result
}

Upvotes: 1

Michael Kessler
Michael Kessler

Reputation: 14235

You have a complete set of events on UIControl (UIButton is its subclass) to handle all the touch events that you need:

enum {
   UIControlEventTouchDown           = 1 <<  0,
   UIControlEventTouchDownRepeat     = 1 <<  1,
   UIControlEventTouchDragInside     = 1 <<  2,
   UIControlEventTouchDragOutside    = 1 <<  3,
   UIControlEventTouchDragEnter      = 1 <<  4,
   UIControlEventTouchDragExit       = 1 <<  5,
   UIControlEventTouchUpInside       = 1 <<  6,
   UIControlEventTouchUpOutside      = 1 <<  7,
   UIControlEventTouchCancel         = 1 <<  8,

   UIControlEventValueChanged        = 1 << 12,

   UIControlEventEditingDidBegin     = 1 << 16,
   UIControlEventEditingChanged      = 1 << 17,
   UIControlEventEditingDidEnd       = 1 << 18,
   UIControlEventEditingDidEndOnExit = 1 << 19,

   UIControlEventAllTouchEvents      = 0x00000FFF,
   UIControlEventAllEditingEvents    = 0x000F0000,
   UIControlEventApplicationReserved = 0x0F000000,
   UIControlEventSystemReserved      = 0xF0000000,
   UIControlEventAllEvents           = 0xFFFFFFFF
};
  • UIControlEventTouchDown will fire when you touch the button
  • UIControlEventTouchDownRepeat will fire if you continue holding the button (notice that this event will fire many times, so you should handle only the first one) - here you should display the popover
  • UIControlEventTouchDragExit will fire when you drag the finger out from the button - here you should hide the popover
  • UIControlEventTouchDragEnter will fire when you drag the finger into the button - here you should display the popover
  • UIControlEventTouchUpInside, UIControlEventTouchUpOutside and UIControlEventTouchCancel will fire when you lift the finger from the button - here you should hide the popover
  • etc.

UPDATE
You will have some logic to implement to handle dragging the finger inside the popover (because then you will drag out from the button) though.

Upvotes: 1

Jakub
Jakub

Reputation: 13860

You have UIControl just for that UILongPressGestureRecognizer And you can set the time of the press by minimumPressDuration property

If you want this behavior on UIButton you have to add this gesture recognizer to the button and handle the first calling (single tap).

Upvotes: 1

Related Questions