0

Sorry if this was already asked/answered.

I'm trying to create a CustomButton (UIView) that detects touch down and up (to customize changes in background color, text color, etc).

I've tried touchesBegan but it reacts too slow.

I've tried UILongPressGestureRecognizer but it doesn't fall back to dragging a UIScrollView below. I mean: when dragging over the button it doesn't drag the scroll view.

Here's my solution so far, using UILongPressGestureRecognizer, with ideas taken from here.

Please, can you think of a better solution than mine? Thanks!

/** View that can be pressed like a button */

import UIKit

class ButtonView : UIView, UIGestureRecognizerDelegate {

    static let NoChange = { (btn:ButtonView) in }

    var enabled = true

    /* Called when the view goes to normal state (set desired appearance) */
    var onNormal = NoChange
    /* Called when the view goes to pressed state (set desired appearance) */
    var onPressed = NoChange
    /* Called when the view is released (perform desired action) */
    var action = {}


    override init(frame: CGRect)
    {
        super.init(frame: frame)

        let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(touched))
        recognizer.delegate = self
        recognizer.minimumPressDuration = 0.0
        addGestureRecognizer(recognizer)
        userInteractionEnabled = true
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        onNormal(self)
    }

    func touched(sender: UILongPressGestureRecognizer)
    {
        guard enabled else { return }

        print("Current state: \(sender.state.rawValue)")

        if sender.state == .Began {
            onPressed(self)
        } else if sender.state == .Ended {
            onNormal(self)
            action()
        } else if sender.state == .Changed {
            onNormal(self)
            // This cancels recognizer when dragging
            // TODO: but doesn't drag scroll view
            sender.enabled = false
            sender.enabled = true
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Usage:

    let button = ButtonView()
    // add some views to the ButtonView, e.g. add a label
    button.onNormal = { $0.backgroundColor = normalColor }
    button.onPressed = { $0.backgroundColor = pressedColor }
    button.action = { doSomethingWhenButtonIsClicked() }
Community
  • 1
  • 1
Ferran Maylinch
  • 10,919
  • 16
  • 85
  • 100
  • GestureRecognizers probably (by definition?) take longer the touches been/eded to know what the user is doing. If touchesBegan() is taking too long, can you re(de)fine the UI in some way that eliminates some of what you are trying to do? In other words, "can you show me a working example of what you want"? You may be looking to do something impossible. –  Nov 21 '16 at 19:18
  • Thanks, basically I have a "vertical" `UIScrollView` and on top I have a couple of views, including my `ButtonView`. I have other views where I use `UITapGestureRecognizer` and they work correctly (I can tap to open something, but I can also drag on top to scroll the UIScrollView). And those views are actually inside another nested "horizontal" UIScrollView, so I can drag both directions to scroll the main vertical scroll view, or the nested horizontal scroll view. I'll investigate if I can solve my problem using `UITapGestureRecognizer`. – Ferran Maylinch Nov 22 '16 at 10:47

1 Answers1

1

Another solution with UIControl. It reacts slower but it's not too bad, and you can drag a UIScrollView below.

/** 
 * View that can be pressed like a button.
 * 
 * Note that, since UIView has userInteractionEnabled = true by default,
 * you should disable it for UIView layers that you put insde this ButtonView.
 * Otherwise the ButtonView will not receive the touch events.
 */

// See: http://stackoverflow.com/q/40726283/custom-button-view

import UIKit

class ButtonView : UIControl {

    static let NoChange = { (btn:ButtonView) in }

    /* Called when the view goes to normal state (set desired appearance) */
    var onNormal = NoChange
    /* Called when the view goes to pressed state (set desired appearance) */
    var onPressed = NoChange
    /* Called when the view goes to disabled state, i.e. enabled = false (set desired appearance) */
    var onDisabled = NoChange
    /* Called when the view is released (perform desired action) */
    var action = {}


    override init(frame: CGRect)
    {
        super.init(frame: frame)

        self.addTarget(self, action: #selector(pressed), forControlEvents: .TouchDown)
        self.addTarget(self, action: #selector(clicked), forControlEvents: .TouchUpInside)
        self.addTarget(self, action: #selector(cancelled), forControlEvents: [.TouchCancel, .TouchDragExit, .TouchUpOutside])

        userInteractionEnabled = true // this is the default!
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        updateBaseStyle()
    }

    func pressed() {
        onPressed(self)
    }

    func clicked() {
        onNormal(self)
        action()
    }

    func cancelled() {
        onNormal(self)
    }

    override var enabled: Bool {
        willSet(e) {
            super.enabled = e
            updateBaseStyle()
        }
    }

    func updateBaseStyle() {
        if self.enabled {
            onNormal(self)
        } else {
            onDisabled(self)
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Usage is similar (I just added onDisable):

let button = ButtonView()
// add some views to the ButtonView, e.g. add a label
// note child views should have userInteractionEnabled = false
button.onNormal = { $0.backgroundColor = normalColor }
button.onPressed = { $0.backgroundColor = pressedColor }
button.onDisabled = { $0.backgroundColor = disabledColor }
button.action = { callToAction() }
Ferran Maylinch
  • 10,919
  • 16
  • 85
  • 100