Получить событие касания для 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)
С этими несколькими изменениями вы должны получить следующее поведение:
(Сообщение выводится на консоль при нажатии любой кнопки.)
Ограничения
Это решение позволяет пользователю вращать колесо как обычно, а также нажимать любую из кнопок. Тем не менее, это не может быть идеальным решением для ваших нужд, так как есть некоторые ограничения:
- Колесо не вращается, если пользователь нажимает на кнопку в пределах какой-либо из кнопок.
- Кнопки можно нажимать, когда колесо находится в движении.
В зависимости от ваших потребностей, вы можете подумать о создании собственного блесна, а не полагаться на сторонний модуль. Трудность с этим модулем состоит в том, что он использует 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
периодически.
Но я настоятельно рекомендую первый подход.