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

отредактированный

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

отредактированный

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

Старый вопрос с более подробной информацией

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

Получить проект здесь: https://github.com/Jasperav/SpinningWheelWithTappableButtons

Посмотрите код ниже того, что я добавил в файл pod:

Я добавил эту переменную в 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, в цикле "для wedgeNumber в"

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, но я подумал, что для этого нужны хит-тесты.

4 ответа

Решение

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

  • В вашем SpinWheelControl класс, вы устанавливаете userInteractionEnabled собственность spinWheelViewс 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,

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

Шаг 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)

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

GIF рабочего вертушки с кнопками

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


Ограничения

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

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

В зависимости от ваших потребностей, вы можете подумать о создании собственного блесна, а не полагаться на сторонний модуль. Трудность с этим модулем состоит в том, что он использует 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)
}

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

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

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

Здесь я покажу вам 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()

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

мотыга

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

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

Другие вопросы по тегам