Search code examples
iosswiftuikituibutton

UIbutton not responding consistently to touch event


I placed my UI button inside stack view of storyboard in UIKit App. and I enabled user interaction to both stack view and button. My button width and height are 20,20. I am using this UIbutton for checkbox sometimes it is responsive and sometimes I have to click multiple times. I tried increasing the size of the box but no luck.when I use tip of my nail it works and if I use my entire finger it won't work. I guess when my finger touches both stackview and button it won't workenter image description here


Solution

  • Posting this answer, as per the OP's comments...

    The issue is unrelated to the button being in a stack view -- the problem, is the size of the button.

    Apple's Human Interface Guidelines states:

    As a general rule, a button needs a hit region of at least 44x44 pt

    That is not a requirement, but as you've seen it can be very tough to consistently tap a 20x20 button.

    Options for a "checkbox button" could include:

    • larger button to begin with
    • larger button frame, with a checkbox image (smaller than the frame)
    • subclassing and using hitTest(_:with:) to expand the touch area
    • using a UI element other than a UIButton and handling the touches

    Up to you to decide which approach fits best with your overall design / UI goals.


    Edit - in response to comment...

    Searching for swift custom checkbox button returns lots of results.

    If you want to look at a very simple example, here is some sample code:

    class CheckBoxButton: UIButton {
        public var isChecked: Bool = false {
            didSet {
                if isChecked {
                    setImage(checkedImg, for: [])
                } else {
                    setImage(unCheckedImg, for: [])
                }
            }
        }
        private var unCheckedImg: UIImage!
        private var checkedImg: UIImage!
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            guard let cImg = UIImage(systemName: "checkmark.square"),
                  let uImg = UIImage(systemName: "square")
            else {
                fatalError("Could not create check box images!!!")
            }
            
            self.unCheckedImg = uImg
            self.checkedImg = cImg
    
            setImage(unCheckedImg, for: [])
        }
    }
    

    class ViewController: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let vStack = UIStackView()
            vStack.axis = .vertical
            vStack.spacing = 8
            
            for i in 0..<5 {
                let hStack = UIStackView()
                hStack.alignment = .center
                hStack.spacing = 8
                let label = UILabel()
                label.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
                label.text = "Option \(i)"
                label.font = .systemFont(ofSize: 28.0, weight: .light)
                let btn = CheckBoxButton()
                btn.backgroundColor = .white
                btn.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
                btn.heightAnchor.constraint(equalTo: btn.widthAnchor).isActive = true
                btn.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
                hStack.addArrangedSubview(label)
                hStack.addArrangedSubview(btn)
                vStack.addArrangedSubview(hStack)
                hStack.isUserInteractionEnabled = true
            }
            
            vStack.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(vStack)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                vStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            ])
    
            // if we want to see the actual button frames...
            //  comment-out this return() line
            return()
            
            for hs in vStack.arrangedSubviews {
                if let hs = hs as? UIStackView,
                   let btn = hs.arrangedSubviews.last as? CheckBoxButton
                {
                    btn.backgroundColor = .yellow
                }
            }
            
        }
        
        @objc func btnTapped(_ sender: UIButton) {
            guard let btn = sender as? CheckBoxButton else { return }
            btn.isChecked.toggle()
        }
    }