【问题标题】:Why is a UIButton consuming touches but not a UIControl为什么 UIButton 消耗触摸而不是 UIControl
【发布时间】:2015-12-03 02:42:20
【问题描述】:
  • 我在表格视图单元格中有一些自定义按钮。
  • 这些按钮包含在另一个不占用整个单元格的视图中。
  • 我希望按钮始终响应点击(并消耗点击,以便不会同时选择单元格)。
  • 我希望我的按钮容器视图能够使用不在按钮本身上的点击(这样单元格就不会被选中)。
  • 我想像往常一样在按钮容器外的单元格中的任何位置选择单元格。

为此,我在按钮容器视图中附加了一个手势识别器。

只要我的按钮是UIButtons(即点击按钮本身会在按钮上引发TouchUpInside 事件,点击按钮容器中的任何其他位置不会执行任何操作并点击按钮容器外的单元格会导致单元格被选中)。但是,如果我使用 UIControl 而不是 UIButton 则不再是这种情况 - 控件永远不会响应点击(按钮容器总是消耗点击并在单元格中的按钮容器外部点击会导致要选择的单元格)。应该注意的是,如果我不向按钮容器添加手势识别器,那么控件会以与UIButton 相同的方式响应点击。

我唯一的解释是UIButton(继承自UIControl)以某种方式增加了一些额外的触摸处理。在这种情况下,我想知道它的作用以及我应该如何模拟它(我需要使用UIControl 而不是UIButton,因为我的按钮有一个我不想玩的自定义视图层次结构UIButton)。

下面的视图控制器代码应该允许任何人重现该问题:

class ViewController: UITableViewController, UIGestureRecognizerDelegate {

    lazy var containerView: UIView = {
        let view: UIView = UIView()
        view.backgroundColor = UIColor.redColor()
        view.setTranslatesAutoresizingMaskIntoConstraints(false)
        view.addSubview(self.buttonContainerView)
        view.addConstraints([
            NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
            NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TrailingMargin, multiplier: 1.0, constant: 0.0),
            NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0.0),
            NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
        ])

        return view
    }()

    lazy var buttonContainerView: UIView = {
        let view: UIView = UIView()
        view.backgroundColor = UIColor.blueColor()
        view.setTranslatesAutoresizingMaskIntoConstraints(false)
        view.addSubview(self.control)
        view.addSubview(self.button)
        view.addConstraints([
            NSLayoutConstraint(item: self.control, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 0.5, constant: 0.0),
            NSLayoutConstraint(item: self.control, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0),
            NSLayoutConstraint(item: self.button, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.5, constant: 0.0),
            NSLayoutConstraint(item: self.button, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0)
        ])

        return view
    }()

    lazy var control: UIControl = {
        let view: UIControl = TestControl(frame: CGRectZero)
        view.addTarget(self, action: Selector("controlTapped:"), forControlEvents: UIControlEvents.TouchUpInside)

        return view
    }()

    lazy var button: UIButton = {
        let view: UIButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
        view.setTitle("Tap button", forState: UIControlState.Normal)
        view.setTranslatesAutoresizingMaskIntoConstraints(false)
        view.addTarget(self, action: Selector("buttonTapped:"), forControlEvents: UIControlEvents.TouchUpInside)

        return view
    }()

    func controlTapped(sender: UIControl) -> Void {
        println("Control tapped!")
    }

    func buttonTapped(sender: UIButton) -> Void {
        println("Button tapped!")
    }

    var recogniser: UITapGestureRecognizer?
    var blocker: UITapGestureRecognizer?

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.rowHeight = 200.0

        self.containerView.layoutMargins = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)

        let recogniser: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedContainer:"))
        recogniser.delegate = self
        self.recogniser = recogniser

        self.containerView.addGestureRecognizer(recogniser)

        let blocker: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedBlocker:"))
        blocker.delegate = self
        self.blocker = blocker

        self.buttonContainerView.addGestureRecognizer(blocker)
    }

    func tappedContainer(recogniser: UIGestureRecognizer) -> Void {
        println("Tapped container!")
    }

    func tappedBlocker(recogniser: UIGestureRecognizer) -> Void {
        println("Tapped blocker!")
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let identifier: String = "identifier"
        let cell: UITableViewCell
        if let queuedCell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(identifier) as? UITableViewCell {
            cell = queuedCell
        }
        else {
            cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: identifier)

            cell.contentView.layoutMargins = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
            cell.contentView.backgroundColor = UIColor.purpleColor()

            cell.contentView.addSubview(self.containerView)

            cell.contentView.addConstraints([
                NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
                NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.TrailingMargin, multiplier: 1.0, constant: 0.0),
                NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0.0),
                NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
            ])
        }

        return cell
    }

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
        println("selected cell")
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
}

class TestControl: UIControl {
    override init(frame: CGRect) {
        super.init(frame: frame)

        let view: UIControl = self

        let label: UILabel = UILabel()
        label.text = "Tap control"
        label.userInteractionEnabled = false

        view.layer.borderColor = UIColor.orangeColor().CGColor
        view.layer.borderWidth = 2.0
        view.setTranslatesAutoresizingMaskIntoConstraints(false)
        label.setTranslatesAutoresizingMaskIntoConstraints(false)

        view.addSubview(label)
        view.addConstraints([
            NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 5.0),
            NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0),
            NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.GreaterThanOrEqual, toItem: view, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
            NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.LessThanOrEqual, toItem: view, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
        ])
    }

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

编辑

需要明确的是,我并不是在寻找“行之有效”的替代解决方案——我想了解这种差异是什么以及我应该做些什么来模仿它,或者可能是另一种语义上正确的方法。

【问题讨论】:

    标签: ios objective-c swift uibutton uicontrol


    【解决方案1】:

    我唯一的解释是 UIButton(继承自 UIControl)以某种方式增加了一些额外的触摸处理。

    你是正确的 UIButton 是特殊的。我在不久前回答related question 时做了一些研究,Event Handling Guide for iOS: Gesture Recognizers'“与其他用户界面控件交互”部分中提到了按钮事件触发的原因:

    在 iOS 6.0 及更高版本中,默认控制操作可防止手势识别器行为重叠。例如,按钮的默认操作是单击。如果您将单击手势识别器附加到按钮的父视图,并且用户点击了该按钮,则该按钮的操作方法将接收触摸事件而不是手势识别器。

    然后它会列出示例,在UIButton 上单击一下就是其中之一。

    像默认控件一样阻止手势识别器的方法是让TestControl 覆盖gestureRecognizerShouldBegin:(请参阅UIView Class Reference)。如果你想模仿UIButton 的行为,你可以使用类似:

    override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
        if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer {
            if tapGestureRecognizer.numberOfTapsRequired == 1 && tapGestureRecognizer.numberOfTouchesRequired == 1 {
                return false;
            }
        }
        return true;
    }
    

    【讨论】:

    • 感谢您的解释和链接——苹果似乎确实告诉我们UIButton 是一个例外(没有告诉我们如何实现类似的行为)。您建议的代码可以完美运行,但是,我想了解原因并确保它不容易更改。 UIControl 没有声明 UIGestureRecognizerDelegate 一致性,所以我是否有效地覆盖了隐藏手势识别器的私有方法?
    • @Rupert gestureRecognizerShouldBegin: 实际上是UIView 上的一个方法(UIControl 继承自),因此不涉及任何隐藏或私有 API。我只是在UIView Class Reference 中仔细检查了该方法的文档,它注意到UISlider 使用它来阻止某些滑动手势,所以这是处理这些情况的官方方法(我将更新我的答案以说明这一点)。
    • @cszucko 出于好奇,您最终是如何解决这个问题的?
    • @SwiftArchitect 我自己还没有遇到过这个特定的场景,但由于鲁珀特说我包含的 sn-p 工作我可能会去。
    • 当然,在答案下发表评论!我将删除我的愚蠢问题,很快就会有这个评论......
    猜你喜欢
    • 1970-01-01
    • 2012-03-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-19
    • 1970-01-01
    相关资源
    最近更新 更多