Получить событие касания для UIButton в UIControl / rotatable view

Отредактировано

См. Раздел комментариев с Натаном, чтобы узнать о последнем проекте. Остается только проблема: получить нужную кнопку.

Отредактировано

Я хочу иметь UIView, который пользователь может вращать. Этот UIView должен содержать несколько кнопок UIButton, которые можно щелкнуть. Мне трудно, потому что я использую подкласс UIControl для создания вращающегося представления, и в этом подклассе я должен отключить взаимодействие с пользователем в подпредставлениях в UIControl (чтобы заставить его вращаться), что может привести к тому, что UIButtons не будет задействован. Как я могу сделать UIView, который пользователь может вращать, и содержащий интерактивные кнопки UIButton? Это ссылка на мой проект, которая дает вам фору: он содержит UIButtons и вращающийся UIView . Однако я не могу нажимать кнопки UIButtons.

Старый вопрос с подробностями

Я использую этот модуль: https://github.com/joshdhenry/SpinWheelControl, и я хочу отреагировать до нажатия кнопок. Я могу добавить кнопку, но не могу получать события касания кнопки. Я использую тесты попаданий, но они никогда не выполняются. Пользователь должен крутить колесо и иметь возможность нажимать кнопку в одном из пирогов.

Загрузите проект здесь: https://github.com/Jasperav/SpinningWheelWithTappableButtons

См. Код ниже, который я добавил в файл модуля:

Я добавил эту переменную в SpinWheelWedge.swift:

let button = SpinWheelWedgeButton()

Я добавил этот класс:

class SpinWheelWedgeButton: TornadoButton {
    public func configureWedgeButton(index: UInt, width: CGFloat, position: CGPoint, radiansPerWedge: Radians) {
        self.frame = CGRect(x: 0, y: 0, width: width, height: 30)
        self.layer.anchorPoint = CGPoint(x: 1.1, y: 0.5)
        self.layer.position = position
        self.transform = CGAffineTransform(rotationAngle: radiansPerWedge * CGFloat(index) + CGFloat.pi + (radiansPerWedge / 2))
        self.backgroundColor = .green
        self.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
    }
    @IBAction func pressed(_ sender: TornadoButton){
        print("hi")
    }
}

Это класс TornadoButton:

class TornadoButton: UIButton{
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        let prespt = self.superview!.layer.convert(suppt, to: pres)
        if (pres.hitTest(suppt)) != nil{
            return self
        }
        return super.hitTest(prespt, with: event)
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        return (pres.hitTest(suppt)) != nil
    }
}

Я добавил это в SpinWheelControl.swift в цикле "для номера клина в"

wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 2, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
wedge.addSubview(wedge.button)

Вот где я думал, что могу получить кнопку в SpinWheelControl.swift:

override open func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    let p = touch.location(in: touch.view)
    let v = touch.view?.hitTest(p, with: nil)
    print(v)
}

Только «v» всегда является колесом прялки, а не кнопкой. Я также не вижу печати кнопок, и проверка результатов никогда не выполняется. Что не так с этим кодом и почему не выполняется hitTest? Я бы предпочел обычный UIBUtton, но я подумал, что для этого нужны хит-тесты.


person J. Doe    schedule 04.10.2017    source источник


Ответы (4)


Мне удалось повозиться с проектом, и я думаю, что у меня есть решение вашей проблемы.

  • В вашем классе SpinWheelControl вы устанавливаете для свойства userInteractionEnabled spinWheelViews значение false. Обратите внимание, что это не совсем то, что вам нужно, потому что вам все еще интересно нажимать кнопку, которая находится внутри spinWheelView. Однако, если вы не отключите взаимодействие с пользователем, колесо не будет вращаться, потому что дочерние просмотры портят прикосновения!
  • Чтобы решить эту проблему, мы можем отключить взаимодействие с пользователем для дочерних представлений и вручную запускать только те события, которые нас интересуют - что в основном touchUpInside для самой внутренней кнопки.
  • Самый простой способ сделать это - использовать endTracking метод SpinWheelControl. Когда вызывается метод endTracking, мы вручную перебираем все кнопки и также вызываем для них endTracking.
  • Теперь проблема с тем, какая кнопка была нажата, остается, потому что мы просто отправили endTracking всем им. Решением этого является переопределение метода endTracking кнопок и запуск метода .touchUpInside вручную только в том случае, если касание hitTest для этой конкретной кнопки было истинным.

Код:

TornadoButton Class: (пользовательские hitTest и pointInside больше не нужны, поскольку мы больше не заинтересованы в обычном тестировании попаданий; мы просто вызываем endTracking напрямую)

class TornadoButton: UIButton{
    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        if let t = touch {
            if self.hitTest(t.location(in: self), with: event) != nil {
                print("Tornado button with tag \(self.tag) ended tracking")
                self.sendActions(for: [.touchUpInside])
            }
        }
    }
}

SpinWheelControl Класс: endTracking метод:

override open func endTracking(_ touch: UITouch?, with event: UIEvent?) {
    for sv in self.spinWheelView.subviews {
        if let wedge = sv as? SpinWheelWedge {
            wedge.button.endTracking(touch, with: event)
        }
    }
    ...
}

Кроме того, чтобы проверить, что вызывается правая кнопка, просто установите тег кнопки равным wedgeNumber при их создании. С помощью этого метода вам не нужно будет использовать настраиваемое смещение, как это делает @nathan, потому что правая кнопка будет реагировать на endTracking, а вы можете просто получить его тег с помощью sender.tag.

person aksh1t    schedule 16.10.2017
comment
Спасибо, см. Мой комментарий о награде. Через 23 часа вы получите 200+ наград. Большое спасибо! - person J. Doe; 16.10.2017

Вот решение для вашего конкретного проекта:

Шаг 1

В функции drawWheel в SpinWheelControl.swift включите взаимодействие с пользователем на spinWheelView. Для этого удалите следующую строку:

self.spinWheelView.isUserInteractionEnabled = false

Шаг 2

Снова в функции drawWheel сделайте кнопку подвидом spinWheelView, а не wedge. Добавьте кнопку в качестве подвида после клина, чтобы она отображалась поверх слоя с формой клина.

Старый:

wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 0.45, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
wedge.addSubview(wedge.button)
spinWheelView.addSubview(wedge)

Новый:

wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 0.45, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
spinWheelView.addSubview(wedge)
spinWheelView.addSubview(wedge.button)

Шаг 3

Создайте новый подкласс UIView, который передает касания своим подпредставлениям.

class PassThroughView: UIView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        for subview in subviews {
            if !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
                return true
            }
        }
        return false
    }
}

Шаг 4

В самом начале функции drawWheel объявите, что spinWheelView имеет тип PassThroughView. Это позволит кнопкам получать события касания.

spinWheelView = PassThroughView(frame: self.bounds)

С этими небольшими изменениями вы должны получить следующее поведение:

гифка рабочего спиннера с кнопками

(Сообщение выводится на консоль при нажатии любой кнопки.)


Ограничения

Это решение позволяет пользователю крутить колесо как обычно, а также нажимать любую из кнопок. Однако это может быть не лучшим решением для ваших нужд, так как есть некоторые ограничения:

  • Колесо не может вращаться, если касание пользователя начинается в пределах любой из кнопок.
  • Кнопки можно нажимать во время движения колеса.

В зависимости от ваших потребностей, вы можете подумать о создании собственного счетчика вместо того, чтобы полагаться на сторонний модуль. Сложность этого модуля заключается в том, что он использует beginTracking(_ touch: UITouch, with event: UIEvent?) и связанные с ним функции вместо распознавателей жестов. Если бы вы использовали распознаватели жестов, было бы легче использовать все UIButton функции.

В качестве альтернативы, если вы просто хотите распознать событие приземления в пределах клина, вы можете продолжить свою hitTest идею.


Изменить: определение того, какая кнопка была нажата.

Если мы знаем selectedIndex колеса и начальное selectedIndex, мы можем вычислить, какая кнопка была нажата.

В настоящее время начальный selectedIndex равен 0, а теги кнопок увеличиваются по часовой стрелке. При нажатии выбранной кнопки (tag = 0) печатается 7, что означает, что кнопки «повернуты» на 7 позиций в своем начальном состоянии. Если бы колесо запускалось в другом положении, это значение было бы другим.

Вот быстрая функция для определения тега кнопки, которая была нажата, с использованием двух частей информации: колеса selectedIndex и subview.tag из текущей point(inside point: CGPoint, with event: UIEvent?) реализации PassThroughView.

func determineButtonTag(selectedIndex: Int, subviewTag: Int) -> Int {
    return subviewTag + (selectedIndex - 7)
}

Опять же, это определенно взлом, но он работает. Если вы планируете и дальше добавлять функциональные возможности к этому элементу управления счетчиком, я настоятельно рекомендую вместо этого создать свой собственный элемент управления, чтобы вы могли спроектировать его с самого начала в соответствии со своими потребностями.

person nathangitter    schedule 15.10.2017
comment
Спасибо за ответ :) он делает почти то, что я хочу делать. Только как вы заявили, колесо не может вращаться, если кнопка нажата. Я действительно хочу, чтобы колесо можно было вращать независимо от того, где находится касание. Я думаю, что это довольно сложно, потому что должна быть проверка, хочет ли пользователь нажать кнопку, чтобы выполнить действие кнопки, ИЛИ просто повернуть UIView. Я использовал стручок, чтобы указать, что я хочу, мне было бы все равно, используется ли что-нибудь еще. Есть ли способ заставить UIView вращаться независимо от касания? - person J. Doe; 15.10.2017
comment
Это сводит воедино то, что проект должен разделить два взаимодействия с пользователем: - он хочет крутить колесо, поэтому он нажимает кнопку, перемещает палец вверх / вниз и отпускает кнопку - ›кнопки не действуют, и вид вращается ИЛИ он нажимает на кнопка, палец не поднимался и не опускался (или слегка) - ›действие кнопки. Спасибо за то, что вы уже сделали. - person J. Doe; 15.10.2017
comment
@ J.Doe Да Я согласен с вашими комментариями выше об оптимальном взаимодействии. Не нашел простого способа решить проблему с раскруткой колеса при запуске касания по кнопке. Он может существовать, но добавит больше сложности и исправлений. Я не уверен, каковы ваши конечные цели, но похоже, что лучший путь вперед - использовать модуль как источник вдохновения для создания собственного пользовательского интерфейса, чтобы вы могли полностью контролировать взаимодействие с пользователем. - person nathangitter; 15.10.2017
comment
См. Этот проект: github.com/Jasperav/. Я внес некоторые изменения, теперь он распечатывает UIButton при касании, он может вращаться, удерживая кнопку, НО ... нажатие UIButton всегда одинаково в той же позиции. Вы можете проверить теги: поверните колесо и нажмите кнопку справа: это всегда кнопка без тега. С помощью hitTest должно быть возможно найти нажатую кнопку (я надеюсь). Мы можем искать кнопку в этом операторе: if! IsSpinning {} в UIView, но я не очень хорошо разбираюсь в hitTest. Я надеюсь, что вы можете помочь. - person J. Doe; 16.10.2017
comment
С помощью приведенного выше комментария я имею в виду, что я дал каждому UIButton тег, чтобы сделать кнопки уникальными. Я хочу распечатать нажатый уникальный UIButton. Теперь он просто печатает ту же кнопку, если вы нажимаете на то же место, независимо от того, вращалось ли колесо. - person J. Doe; 16.10.2017
comment
@ J.Doe Я понимаю, о чем вы. Одно из решений (хотя и не самое чистое) - сравнить значение тега с selectedIndex элемента управления прялкой. Если вы возьмете разницу между начальным индексом и текущим selectedIndex, вы сможете определить, какая кнопка была нажата. - person nathangitter; 16.10.2017
comment
Хм, я не совсем понимаю, что вы имеете в виду. Я стараюсь изо всех сил с хит-тестами. Это действительно последний шаг в ответе, странно, что в хит-тестах не указана правильная кнопка. - person J. Doe; 16.10.2017
comment
@ J.Doe Я добавил лучшее описание моего последнего комментария. - person nathangitter; 16.10.2017
comment
Спасибо, Натан! Я наградил вас существующей наградой. См. Мой комментарий о награде. - person J. Doe; 16.10.2017
comment
как добавить пробелы между клиньями @nathan - person Asim Ifitkhar Abbasi; 27.11.2017

Общее решение - использовать UIView и разместить все ваши UIButton там, где они должны быть, и использовать UIPanGestureRecognizer для поворота вашего обзора, вычисления скорости и вектора направления и поворота вашего обзора. Для поворота вашего представления я предлагаю использовать transform, потому что он может быть анимирован, а также ваши дочерние представления также будут повернуты. (дополнительно: если вы хотите установить направление ваших UIButtons всегда вниз, просто поверните их в обратном направлении, они всегда будут смотреть вниз)

Взломать

Некоторые люди также используют UIScrollView вместо UIPanGestureRecognizer. Поместите описанный View внутрь UIScrollView и используйте методы делегата UIScrollView для вычисления скорости и направления, а затем примените эти значения к вашему UIView, как описано. Причина этого взлома в том, что UIScrollView автоматически снижает скорость и обеспечивает лучший опыт. (Используя эту технику, вы должны установить contentSize на что-то очень большое и периодически перемещать contentOffset из UIScrollView на .zero.

Но я настоятельно рекомендую первый подход.

person farzadshbfn    schedule 08.10.2017
comment
Можете ли вы показать мне пример первого описанного вами метода? Разве не сложно рассчитать скорость с помощью UIPanGesture и с его помощью повернуть представление? Почему вы не рекомендуете второй способ? - person J. Doe; 13.10.2017

На мой взгляд, вы можете использовать собственное представление с несколькими подслоями и всем остальным, что вам нужно. В этом случае вы получите полную гибкость, но вам также следует написать немного больше кода.

Если вам нравится этот вариант, вы можете получить что-то вроде изображения на гифке ниже (вы можете настроить его по своему усмотрению - добавить текст, изображения, анимацию и т. Д.):

введите описание изображения здесь

Здесь я показываю вам 2 непрерывных панорамирования и одно нажатие на фиолетовую секцию - при обнаружении касания цвет 6 bg меняется на зеленый

Чтобы обнаружить касание, я использовал touchesBegan, как показано ниже.

Чтобы поиграть с кодом для этого, вы можете скопировать и вставить код ниже на игровую площадку и изменить в соответствии с вашими потребностями.

//: A UIKit based Playground for presenting user interface


import UIKit import PlaygroundSupport

class RoundView : UIView {
    var sampleArcLayer:CAShapeLayer = CAShapeLayer()

    func performRotation( power: Float) {

        let maxDuration:Float = 2
        let maxRotationCount:Float = 5

        let currentDuration = maxDuration * power
        let currrentRotationCount = (Double)(maxRotationCount * power)

        let fromValue:Double = Double(atan2f(Float(transform.b), Float(transform.a)))
        let toValue = Double.pi * currrentRotationCount + fromValue

        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotateAnimation.fromValue = fromValue
        rotateAnimation.toValue = toValue
        rotateAnimation.duration = CFTimeInterval(currentDuration)
        rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        rotateAnimation.isRemovedOnCompletion = true

        layer.add(rotateAnimation, forKey: nil)
        layer.transform = CATransform3DMakeRotation(CGFloat(toValue), 0, 0, 1)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        drawLayers()
    }

    private func drawLayers()
    {
        sampleArcLayer.removeFromSuperlayer()
        sampleArcLayer.frame = bounds
        sampleArcLayer.fillColor = UIColor.purple.cgColor

        let proportion = CGFloat(20)
        let centre = CGPoint (x: frame.size.width / 2, y: frame.size.height / 2)
        let radius = frame.size.width / 2
        let arc = CGFloat.pi * 2 * proportion / 100 // i.e. the proportion of a full circle

        let startAngle:CGFloat = 45
        let cPath = UIBezierPath()
        cPath.move(to: centre)
        cPath.addLine(to: CGPoint(x: centre.x + radius * cos(startAngle), y: centre.y + radius * sin(startAngle)))
        cPath.addArc(withCenter: centre, radius: radius, startAngle: startAngle, endAngle: arc + startAngle, clockwise: true)
        cPath.addLine(to: CGPoint(x: centre.x, y: centre.y))
        sampleArcLayer.path = cPath.cgPath

        // you can add CATExtLayer and any other stuff you need

        layer.addSublayer(sampleArcLayer)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let point = touches.first?.location(in: self) {
            if let layerArray = layer.sublayers {
                for sublayer in layerArray {
                    if sublayer.contains(point) {
                        if sublayer == sampleArcLayer {
                            if sampleArcLayer.path?.contains(point) == true {
                                backgroundColor = UIColor.green
                            }
                        }
                    }
                }
            }
        }
    }

}

class MyViewController : UIViewController {


    private var lastTouchPoint:CGPoint = CGPoint.zero
    private var initialTouchPoint:CGPoint = CGPoint.zero
    private let testView:RoundView = RoundView(frame:CGRect(x: 40, y: 40, width: 100, height: 100))

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor.white

        testView.layer.cornerRadius = testView.frame.height / 2
        testView.layer.masksToBounds = true
        testView.backgroundColor = UIColor.red
        view.addSubview(testView)

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(MyViewController.didDetectPan(_:)))
        testView.addGestureRecognizer(panGesture)
    }

    @objc func didDetectPan(_ gesture:UIPanGestureRecognizer) {

        let touchPoint = gesture.location(in: testView)
        switch gesture.state {
        case .began:
            initialTouchPoint = touchPoint
            break
        case .changed:
            lastTouchPoint = touchPoint
            break
        case .ended, .cancelled:
            let delta = initialTouchPoint.y - lastTouchPoint.y
            let powerPercentage =  max(abs(delta) / testView.frame.height, 1)
            performActionOnView(scrollPower: Float(powerPercentage))
            initialTouchPoint = CGPoint.zero
            break
        default:
            break
        }
    }

    private func performActionOnView(scrollPower:Float) {
        testView.performRotation(power: scrollPower)
    } } // Present the view controller in the Live View window 
      PlaygroundPage.current.liveView = MyViewController()
person hbk    schedule 13.10.2017
comment
Привет, спасибо за вашу поддержку. Возможно, я недостаточно четко сформулировал свой вопрос: пользователь должен «вращать» представление, скользя пальцем по нему, и представление должно перемещаться в соответствии с движением его пальца. Мой проект, который находится в моем вопросе, описывает представление, которое я имею в виду: вращаемое представление, которое пользователь может вращать, и представление вращается только тогда, когда пользователь касается представления. Я просто хочу добавить внутрь несколько кнопок, которые могут получать события касания. Этот код несколько раз вращается случайным образом и не содержит кнопок, хотя их можно легко добавить и, возможно, они получат событие касания. - person J. Doe; 14.10.2017
comment
Я надеюсь, что вы сможете заставить свое представление реагировать на прикосновения пользователей и позволить вашему представлению вращаться, как в примере, который я добавил с нажимаемыми кнопками. - person J. Doe; 14.10.2017