I’d like a ball to track my finger as I drag it along a circular trajectory for every allowable device orientation on iPhone or iPad. Views appear to be correctly centred when a device is rotated but the ball will not stay on the circumference and seems to go anywhere when I drag it.
EDIT
Martin R's answer now displays this as required. My only additional code change was to remove an unnecessary declaration var shapeLayer = CAShapeLayer()
The maths in this example made perfect sense until I tried constraining both ball and trajectory to the view's centre and adding the ball’s centre coordinates as offsets at run time. I followed these recommendations on how to constrain a view.
There are three things I don’t understand.
First, calculating the circle’s circumference from two variables trackRadius
and angle theta
and using sin
and cos
of theta
to find x
and y
coordinates will not place the ball in the right position.
Second, using atan
to find the angle theta
between the view centre and the point touched, and using trackRadius
with theta
to find x
and y
coordinates will not place or move the ball to a new place along the circumference.
And third, whenever I drag the ball, a message in the debug area says that Xcode is Unable to simultaneously satisfy constraints
, although no constraints problems are reported prior to dragging it.
There may be more than one problem here. My brain is starting to hurt and I’d be grateful if someone could point out what I have done wrong.
Here is my code.
import UIKit
class ViewController: UIViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .all }
var shapeLayer = CAShapeLayer()
let track = ShapeView()
var ball = ShapeView()
var theta = CGFloat()
private let trackRadius: CGFloat = 125
private let ballRadius: CGFloat = 10
override func viewDidLoad() {
super.viewDidLoad()
createTrack()
createBall()
}
private func createTrack() {
track.translatesAutoresizingMaskIntoConstraints = false
track.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: -trackRadius, y: -trackRadius, width: 2 * trackRadius, height: 2 * trackRadius)).cgPath
track.shapeLayer.fillColor = UIColor.clear.cgColor
track.shapeLayer.strokeColor = UIColor.red.cgColor
view.addSubview(track)
track.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
track.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
private func createBall() {
let offset = placeBallOnCircumference()
drawBall()
constrainBall(offset: offset)
let touch = UIPanGestureRecognizer(target: self, action:#selector(dragBall(recognizer:)))
view.addGestureRecognizer(touch)
}
private func placeBallOnCircumference() -> CGPoint {
let theta: Double = 0 // at 0 radians
let x = CGFloat(cos(theta)) * trackRadius // find x and y coords on
let y = CGFloat(sin(theta)) * trackRadius // circle circumference
return CGPoint(x: x, y: y)
}
func dragBall(recognizer: UIPanGestureRecognizer) {
var offset = CGPoint()
let finger : CGPoint = recognizer.location(in: self.view)
theta = CGFloat(atan2(Double(finger.x), Double(finger.y))) // get angle from finger tip to centre
offset.x = CGFloat(cos(theta)) * trackRadius // use angle and radius to get x and
offset.y = CGFloat(sin(theta)) * trackRadius // y coords on circle circumference
drawBall()
constrainBall(offset: offset)
}
private func drawBall() {
ball.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 2 * ballRadius, height: 2 * ballRadius)).cgPath
ball.shapeLayer.fillColor = UIColor.cyan.cgColor
ball.shapeLayer.strokeColor = UIColor.black.cgColor
view.addSubview(ball)
}
private func constrainBall(offset: CGPoint) {
ball.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
ball.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: offset.x),
ball.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: offset.y),
ball.widthAnchor.constraint(equalToConstant: trackRadius),
ball.heightAnchor.constraint(equalToConstant: trackRadius)
])
}
}
The main error is that
theta = CGFloat(atan2(Double(finger.x), Double(finger.y))) // get angle from finger tip to centre
does not take the views (or track) center into account, and that the arguments to atan2()
are the wrong way around (y comes first). It should be:
theta = atan2(finger.y - track.center.y, finger.x - track.center.x)
Another problem is that you add more and more contraints
in func constrainBall()
, without removing the previous ones.
You should keep references to the constraints and modify them instead.
Finally note that the width/height constraint for the ball should be 2*ballRadius
, not trackRadius
.
Putting it all together (and removing some unnecessary type conversions), it would look like this:
var ballXconstraint: NSLayoutConstraint!
var ballYconstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
createTrack()
createBall()
let touch = UIPanGestureRecognizer(target: self, action:#selector(dragBall(recognizer:)))
view.addGestureRecognizer(touch)
}
private func createTrack() {
track.translatesAutoresizingMaskIntoConstraints = false
track.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 2 * trackRadius, height: 2 * trackRadius)).cgPath
track.shapeLayer.fillColor = UIColor.clear.cgColor
track.shapeLayer.strokeColor = UIColor.red.cgColor
view.addSubview(track)
track.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
track.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
track.widthAnchor.constraint(equalToConstant: 2 * trackRadius).isActive = true
track.heightAnchor.constraint(equalToConstant: 2 * trackRadius).isActive = true
}
private func createBall() {
// Create ball:
ball.translatesAutoresizingMaskIntoConstraints = false
ball.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 2 * ballRadius, height: 2 * ballRadius)).cgPath
ball.shapeLayer.fillColor = UIColor.cyan.cgColor
ball.shapeLayer.strokeColor = UIColor.black.cgColor
view.addSubview(ball)
// Width/Height contraints:
ball.widthAnchor.constraint(equalToConstant: 2 * ballRadius).isActive = true
ball.heightAnchor.constraint(equalToConstant: 2 * ballRadius).isActive = true
// X/Y constraints:
let offset = pointOnCircumference(0.0)
ballXconstraint = ball.centerXAnchor.constraint(equalTo: track.centerXAnchor, constant: offset.x)
ballYconstraint = ball.centerYAnchor.constraint(equalTo: track.centerYAnchor, constant: offset.y)
ballXconstraint.isActive = true
ballYconstraint.isActive = true
}
func dragBall(recognizer: UIPanGestureRecognizer) {
let finger = recognizer.location(in: self.view)
// Angle from track center to touch location:
theta = atan2(finger.y - track.center.y, finger.x - track.center.x)
// Update X/Y contraints of the ball:
let offset = pointOnCircumference(theta)
ballXconstraint.constant = offset.x
ballYconstraint.constant = offset.y
}
private func pointOnCircumference(_ theta: CGFloat) -> CGPoint {
let x = cos(theta) * trackRadius
let y = sin(theta) * trackRadius
return CGPoint(x: x, y: y)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With