How to constraint a view/layer's move path to a Bezier curve path? - ios

Before asking the question, i have searched the SO:
iPhone-Move UI Image view along a Bezier curve path
But it did not give a explicit answer.
My requirement is like this, I have a bezier path, and a view(or layer if OK), I want to add pan gesture to the view, and the view(or layer)'s move path must constraint to the bezier path.
My code is below:
The MainDrawView.swift:
import UIKit
class MainDrawView: UIView {
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
drawArc()
}
func drawArc() {
let context = UIGraphicsGetCurrentContext()
// set start point
context?.move(to: CGPoint.init(x: 0, y: 400))
//draw curve
context?.addQuadCurve(to: CGPoint.init(x: 500, y: 250), control: CGPoint.init(x: UIScreen.main.bounds.size.width, y: 200))
context?.strokePath()
}
}
The ViewController.swift:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var clickButton: UIButton!
lazy var view1:UIView = {
let view: UIView = UIView.init(frame: CGRect.init(origin: CGPoint.init(x: 0, y: 0), size: CGSize.init(width: 10, height: 10)))
view.center = CGPoint.init(x: UIScreen.main.bounds.size.width / 2.0, y: UIScreen.main.bounds.size.height / 2.0)
view.backgroundColor = UIColor.red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
initData()
initUI()
}
func initData() {
}
func initUI() {
self.clickButton.isHidden = true
// init the view
self.view.addSubview(self.view1)
}
}
Edit -1
My deep requirment is when I use pan guester move the view/layer, the view will move on the bezier path.

If bezier path is off lines then u can find the slope of the line and for every change in x or y you can calculate the position of the bezier path
var y :Float = (slope * (xPos-previousCoord.x))+previousCoord.y; xPos is continuously changing. Similarly, u can find for x. For any closed shape with line segments, you can use this.
But if u need it for circle, then u need to convert cartesian to polar. i.e.., from coordinates u can find the angle, then from that angle, you have the radius so by using that angle u need to find cartesian coordinates from that.
θ = tan-1 ( 5 / 12 )
U need to use mainly 3 coordinates one is centre of circle, the second one is your touch point, and the last one is (touchpoint.x, centreofcircle.y). from centre of circle calculate distance between two coordinates
You have θ and radius of circle then find points using
x = r × cos( θ )
y = r × sin( θ )
Don't mistake r in the image for the radius of the circle, r in the image is the hypotenuse of three coordinates. You should calculate for every change of x in touch point.
Hope it works. But for irregular shapes I am not sure how to find.

It sounds like you'll probably have to do some calculations. When you set your method to handle the UIPanGestureRecognizer, you can get a vector back for the pan, something like this:
func panGestureRecognized(_ sender: UIPanGestureRecognizer) {
let vector:CGPoint = sender.translation(in: self.view) // Or use whatever view the UIPanGestureRecognizer was added to
}
You could then use that to extrapolate the movement along the x and y axis. I would recommend starting by getting an algebraic equation for the path you're moving the view on. To move the view along that line, you're going to have to be able to calculate a point along that line relative to the movement of the UIPanGestureRecognizer, and then update the position of the view using that calculated point along the line.
I don't know if it'll work for what you're trying to do, but if you want to try animating your view along the path, it's actually pretty easy:
let path = UIBezierPath() // This would be your custom path
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path.cgPath
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false animation.repeatCount = 0
animation.duration = 5.0 // However long you want
animation.speed = 2 // However fast you want
animation.calculationMode = kCAAnimationPaced
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animatingView.layer.add(animation, forKey: "moveAlongPath")

Related

How can I get the coordinates of a Label inside a view?

What I am trying to do is to get the position of my label (timerLabel) in order to pass those coordinates to UIBezierPath (so that the center of the shape and the center of the label coincide).
Here's my code so far, inside the viewDidLoad method, using Xcode 13.2.1:
// getting the center of the label
let center = CGPoint.init(x: timerLabel.frame.midX , y: timerLabel.frame.midY)
// drawing the shape
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
and this is what I have when I run my app:
link
What I don't understand is why I get (0,0) as coordinates even though I access the label's property (timerLabel.frame.midX).
The coordinates of your label may vary depending on current layout. You need to track all changes and reposition your circle when changes occur. In view controller that uses constraints you would override
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// recreate your circle here
}
this alone does not explain why your circle is so far out. First of all, looking at your image you do not get (0, 0) but some other value which may be relative position of your label within the blue bubble. The frame is always relative to its superview so you need to convert that into your own coordinate system:
let targetView = self.view!
let sourceView = timerLabel!
let centerOfSourceViewInTargetView: CGPoint = targetView.convert(CGPoint(x: sourceView.bounds.midX, y: sourceView.bounds.midY), to: targetView)
// Use centerOfSourceViewInTargetView as center
but I suggest using neither of the two. If you are using constraints (which you should) then rather create more views than adding layers to your existing views.
For instance you could try something like this:
#IBDesignable class CircleView: UIView {
#IBInspectable var lineWidth: CGFloat = 10 { didSet { refresh() } }
#IBInspectable var strokeColor: UIColor = .lightGray { didSet { refresh() } }
override var frame: CGRect { didSet { refresh() } }
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let fillRadius: CGFloat = min(bounds.width, bounds.height)*0.5
let strokeRadius: CGFloat = fillRadius - lineWidth*0.5
let path = UIBezierPath(ovalIn: .init(x: bounds.midX-strokeRadius, y: bounds.midY-strokeRadius, width: strokeRadius*2.0, height: strokeRadius*2.0))
path.lineWidth = lineWidth
strokeColor.setStroke()
UIColor.clear.setFill() // Probably not needed
path.stroke()
}
private func refresh() {
setNeedsDisplay() // This is to force redraw
}
}
this view should draw your circle within itself by overriding draw rect method. You can easily use it in your storyboard (first time it might not draw in storyboard because Xcode. Simply close your project and reopen it and you should see the circle even in storyboard).
Also in storyboard you can directly modify both line width and stroke color which is very convenient.
About the code:
Using #IBDesignable to see drawing in storyboard
Using #IBInspectable to be able to set values in storyboard
Refreshing on any value change to force redraw (sometimes needed)
When frame changes forcing a redraw (Needed when setting frame from code)
A method layoutSubviews is called when resized from constraints. Again redrawing.
Path is computed so that it fits within the size of view.

Animate CAShapeLayer path with CASpringAnimation

I have a custom UIView where I add a CAShapeLayer as a direct sublayer of the view's layer:
private let arcShapeLayer = CAShapeLayer()
The CAShapeLayer has the following properties (the border is to visualize more easily the animation) declared in the awakeFromNib:
arcShapeLayer.lineWidth = 2.0
arcShapeLayer.strokeColor = UIColor.black.cgColor
arcShapeLayer.fillColor = UIColor.lightGray.cgColor
arcShapeLayer.masksToBounds = false
layer.masksToBounds = false
layer.addSublayer(arcShapeLayer)
I declare the frame of the arcShapeLayer in the layoutSubviews:
arcShapeLayer.frame = layer.bounds
Then I have the following function where I pass a certain percentage (from the ViewController) in order to achieve the arc effect after some dragging. Simply put I add a quadCurve at the bottom of the layer:
func configureArcPath(curvePercentage: CGFloat) -> CGPath {
let path = UIBezierPath()
path.move(to: CGPoint.zero)
path.addLine(to:
CGPoint(
x: arcShapeLayer.bounds.minX,
y: arcShapeLayer.bounds.maxY
)
)
path.addQuadCurve(
to:
CGPoint(
x: arcShapeLayer.bounds.maxX,
y: arcShapeLayer.bounds.maxY
),
controlPoint:
CGPoint(
x: arcShapeLayer.bounds.midX,
y: arcShapeLayer.bounds.maxY + arcShapeLayer.bounds.maxY * (curvePercentage * 0.4)
)
)
path.addLine(to:
CGPoint(
x: arcShapeLayer.bounds.maxX,
y: arcShapeLayer.bounds.minY
)
)
path.close()
return path.cgPath
}
Then I try to animate with a spring effect the arc with the following code:
func animateArcPath() {
let springAnimation = CASpringAnimation(keyPath: "path")
springAnimation.initialVelocity = 10
springAnimation.mass = 10
springAnimation.duration = springAnimation.settlingDuration
springAnimation.fromValue = arcShapeLayer.path
springAnimation.toValue = configureArcPath(curvePercentage: 0.0)
springAnimation.fillMode = .both
arcShapeLayer.add(springAnimation, forKey: nil)
arcShapeLayer.path = configureArcPath(curvePercentage: 0.0)
}
The problem, as you can see in the video, is that the arc never overshoots. Although it oscillates between its original position and the rest position, the spring effect is never achieved.
What am I missing here?
I played around with this and I can report that you are absolutely right. It has nothing to do clamping at zero. The visible animation clamps at both extremes.
Let's say you supply a big mass value so that the overshoot should go way past the initial position of the convex bow on its return journey back to the start. Then what we see is that the animation clamps both at that original position and at the minimum you are trying to animate to. As the spring comes swinging between them, we see the animation happening between one extreme and the other, but as it reaches the max or min it just clamps there until it has had time to swing to its full extent and return.
I have to conclude that CAShapeLayer path doesn't like springing animations. This same point is raised at CAShapeLayer path spring animation not 'overshooting'
I was able to simulate the sort of look you're probably after by chaining normal basic animations:
Here's the code I used (based on your own code):
#IBAction func animateArcPath() {
self.animate(to:-1, in:0.5, from:arcShapeLayer.path!)
}
func animate(to arc:CGFloat, in time:Double, from current:CGPath) {
let goal = configureArcPath(curvePercentage: arc)
let anim = CABasicAnimation(keyPath: "path")
anim.duration = time
anim.fromValue = current
anim.toValue = goal
anim.delegate = self
anim.setValue(arc, forKey: "arc")
anim.setValue(time, forKey: "time")
anim.setValue(goal, forKey: "pathh")
arcShapeLayer.add(anim, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let arc = anim.value(forKey:"arc") as? CGFloat,
let time = anim.value(forKey:"time") as? Double,
let p = anim.value(forKey:"pathh") {
if time < 0.05 {
return
}
self.animate(to:arc*(-0.5), in:time*0.5, from:p as! CGPath)
}
}

CGAffineTransform an UIView with UIPanGestureRecognizer

I have view A, add sub on self view. And i want when i draw panGestureRegonizer on view A, view A will move follow draw.
And while moving view A will scale. View A will scale to smaller when view move to top/left/bottom/right of sreen and scale to larger when view move to center of screen.
I have try many solution, but i can not make it.
Please suggest help me?
Logic: You have coordinate system(CS) with center, x and y. When the user uses pan gesture, he/she generates sequence of points in the CS. So our task is to measure the distance between the center of the CS and users' points. When we have the furthest distance, we can calculate scale factor for our scaling view.
var center: CGPoint! //center of the CS
let maxSize: CGSize = CGSize.init(width: 100, height: 100) // maximum size of our scaling view
var maxLengthToCenter: CGFloat! //maximum distance from the center of the CS to the furthest point in the CS
private func prepareForScaling() {
self.center = self.view.center //we set the center of our CS to equal the center of the VC's view
let frame = self.view.frame
//the furthest distance in the CS is the diagonal, and we calculate it using pythagoras theorem
self.maxLengthToCenter = (frame.width*frame.width + frame.height*frame.height).squareRoot()
}
Then we need to call our setup functional to have our data ready for scaling functionality - we can do this in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.prepareForScaling()
}
Then we need a helper function to calculates the scaled size of our view, for user's pan gesture current position on the screen.
private func scaledSize(for location: CGPoint) -> CGSize {
//calculate location x,y differences from the center
let xDifference = location.x - self.center.x
let yDifference = location.y - self.center.y
//calculate the scale factor - note that this factor will be between 0.0(center) and 0.5(diagonal - furthest point)
//It is due our measurement - from center to view's edge. Consider multiplying this factor with your custom constant.
let scaleFactor = (xDifference*xDifference + yDifference*yDifference).squareRoot()/maxLengthToCenter
//create scaled size with maxSize and current scale factor
let scaledSize = CGSize.init(width: maxSize.width*(1-scaleFactor), height: maxSize.height*(1-scaleFactor))
return scaledSize
}
And finally, we need to modify our pan gesture action to change the size of A:
#IBAction func panGestureAction(_ sender: UIPanGestureRecognizer) {
UIView.animateKeyframes(withDuration: 0.1, delay: 0, options: UIViewKeyframeAnimationOptions.calculationModeLinear, animations: {
let location = sender.location(in: sender.view?.superview)
sender.view?.frame = CGRect.init(origin: CGPoint.init(x: 0, y: 0), size: self.scaledSize(for: location))
sender.view?.center = location
})
}

iOS: Animating a circle slice into a wider one

Core-Animation treats angles as described in this image:
(image from http://btk.tillnagel.com/tutorials/rotation-translation-matrix.html)
EDIT: Adding an animated gif to explain better what I'm needing:
I need to animate a slice to grow wider, starting at 300:315 degrees, and ending 300:060.
To create each slice I'm using this function:
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
func createSlice(angle1:CGFloat, angle2:CGFloat) -> UIBezierPath! {
let path: UIBezierPath = UIBezierPath()
let width: CGFloat = self.frame.size.width/2
let height: CGFloat = self.frame.size.height/2
let centerToOrigin: CGFloat = sqrt((height)*(height)+(width)*(width));
let ctr: CGPoint = CGPoint(x: width, y: height)
path.move(to: ctr)
path.addArc( withCenter: ctr,
radius: centerToOrigin,
startAngle: CGFloat(angle1).toRadians(),
endAngle: CGFloat(angle2).toRadians(),
clockwise: true
)
path.close()
return path
}
I can now create the two slices and a sublayer with the smaller one, but I can't find how to proceed from this point:
func doStuff() {
path1 = self.createSlice(angle1: 300,angle2: 315)
path2 = self.createSlice(angle1: 300,angle2: 60)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path1.cgPath
shapeLayer.fillColor = UIColor.cyan.cgColor
self.layer.addSublayer(shapeLayer)
I would highly appreciate any help here!
Only a single color
If you want to animate the angle of a solid color filled pie segment like the one in your question, then you can do it by animating the strokeEnd of a CAShapeLayer.
The "trick" here is to make a very wide line. More specifically, you can create a path that is just an arc (the dashed line in the animation below) at half of the intended radius and then giving it the full radius as its line width. When you animate stroking that line it looks like the orange segment below:
Depending on your use case, you can either:
create a path from one angle to the other angle and animate stroke end from 0 to 1
create a path for a full circle, set stroke start and stroke end to some fraction of the circle, and animate stroke end from the start fraction to the end fraction.
If your drawing is just a single color like this, then this will be the smallest solution to your problem.
However, if your drawing is more complex (e.g. also stroking the pie segment) then this solutions simply won't work and you'll have to do something more complex.
Custom drawing / Custom animations
If your drawing of the pie segment is any more complex, then you'll quickly find yourself having to create a layer subclass with custom animatable properties. Doing so is a bit more code - some of which might look a bit unusual1 - but not as scary as it might sound.
This might be one of those things that is still more convenient to do in Objective-C.
Dynamic properties
First, create a layer subclass with the properties you're going to need. In Objective-C parlance these properties should be #dynamic, i.e. not synthesized. This isn't the same as dynamic in Swift. Instead we have to use #NSManaged.
class PieSegmentLayer : CALayer {
#NSManaged var startAngle, endAngle, strokeWidth: CGFloat
#NSManaged var fillColor, strokeColor: UIColor?
// More to come here ...
}
This allows Core Animation to handle these properties dynamically allowing it to track changes and integrate them into the animation system.
Note: a good rule of thumb is that these properties should all be related to drawing / visual presentation of the layer. If they aren't then it's quite likely that they don't belong on the layer. Instead they could be added to a view that in turn uses the layer for its drawing.
Copying layers
During the custom animation, Core Animation is going to want to create and render different layer configurations for different frames. Unlike most of Apple's other frameworks, this happens using the copy constructor init(layer:). For the above five properties to be copied along, we need to override init(layer:) and copy over their values.
In Swift we also have to override the plain init() and init?(coder).
override init(layer: Any) {
super.init(layer: layer)
guard let other = layer as? PieSegmentLayer else { return }
fillColor = other.fillColor
strokeColor = other.strokeColor
startAngle = other.startAngle
endAngle = other.endAngle
strokeWidth = other.strokeWidth
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
return nil
}
Reacting to change
Core Animation is in many ways built for performance. One of the ways it achieves this is by avoiding unnecessary work. By default, a layer won't redraw itself when a property changes. But these properties is used for drawing, and we want the layer to redraw when any of them changes. To do that, we need to override needsDisplay(forKey:) and return true if the key was one of these properties.
override class func needsDisplay(forKey key: String) -> Bool {
switch key {
case #keyPath(startAngle), #keyPath(endAngle),
#keyPath(strokeWidth),
#keyPath(fillColor), #keyPath(strokeColor):
return true
default:
return super.needsDisplay(forKey: key)
}
}
Additionally, If we want the layers default implicit animations for these properties, we need to override action(forKey:) to return a partially configured animation object. If we only want some properties (e.g. the angles) to implicitly animate, then we only need to return an animation for those properties. Unless we need something very custom, it's good to just return a basic animation with the fromValue set to the current presentation value:
override func action(forKey key: String) -> CAAction? {
switch key {
case #keyPath(startAngle), #keyPath(endAngle):
let anim = CABasicAnimation(keyPath: key)
anim.fromValue = presentation()?.value(forKeyPath: key)
return anim
default:
return super.action(forKey: key)
}
}
Drawing
The last piece of a custom animation is the custom drawing. This is done by overriding draw(in:) and using the supplied context to draw the layer:
override func draw(in ctx: CGContext) {
let center = CGPoint(x: bounds.midX, y: bounds.midY)
// subtract half the stroke width to avoid clipping the stroke
let radius = min(center.x, center.y) - strokeWidth / 2
// The two angle properties are in degrees but CG wants them in radians.
let start = startAngle * .pi / 180
let end = endAngle * .pi / 180
ctx.beginPath()
ctx.move(to: center)
ctx.addLine(to: CGPoint(x: center.x + radius * cos(start),
y: center.y + radius * sin(start)))
ctx.addArc(center: center, radius: radius,
startAngle: start, endAngle: end,
clockwise: start > end)
ctx.closePath()
// Configure the graphics context
if let fillCGColor = fillColor?.cgColor {
ctx.setFillColor(fillCGColor)
}
if let strokeCGColor = strokeColor?.cgColor {
ctx.setStrokeColor(strokeCGColor)
}
ctx.setLineWidth(strokeWidth)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
// Draw
ctx.drawPath(using: .fillStroke)
}
Here I've filled and stroked a pie segment that extends from the center of the layer to the nearest edge. You should replace this with your custom drawing.
A custom animation in action
With all that code in place, we now have a custom layer subclass whose properties can be animated both implicitly (just by changing them) and explicitly (by adding a CAAnimation for their key). The results looks something like this:
Final words
It might not be obvious with the frame rate of those animations but one strong benefit from leveraging Core Animation (in different ways) in both these solutions is that it decouples the drawing of a single state from the timing of an animations.
That means that the layer doesn't know and doesn't have to know about the duration, delays, timing curves, etc. These can all be configured and controlled externally.
So at last I have found a solution. It took me time to understand that there is indeed no way to animate the fill of the shape, but we can trick CA engine by creating a filled circle by making the stroke (i.e. the border of the arc) extremely wide, so that it fills the whole circle!
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
import UIKit
class SliceView: UIView {
let circleLayer = CAShapeLayer()
var fromAngle:CGFloat = 30
var toAngle:CGFloat = 150
var color:UIColor = UIColor.magenta
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
convenience init(frame:CGRect, fromAngle:CGFloat, toAngle:CGFloat, color:UIColor) {
self.init(frame:frame)
self.fromAngle = fromAngle
self.toAngle = toAngle
self.color = color
}
func setup() {
circleLayer.strokeColor = color.cgColor
circleLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(circleLayer)
layer.backgroundColor = UIColor.brown.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
let startAngle:CGFloat = (fromAngle-90).toRadians()
let endAngle:CGFloat = (toAngle-90).toRadians()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 4
let path = UIBezierPath(arcCenter: CGPoint(x: 0,y :0), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
circleLayer.position = center
circleLayer.lineWidth = radius*2
circleLayer.path = path.cgPath
}
public func animate() {
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 3.0;
pathAnimation.fromValue = 0.0;
pathAnimation.toValue = 1.0;
circleLayer.add(pathAnimation, forKey: "strokeEndAnimation")
}
}
So, now we can add it into our view controller and run the animation. In my case - I'm bridging it into Objecive-C but you can easily adapt it to swift.
I simply can't believe that in 2017 it was still not possible to find a ready solution for this simple task. It took me days to have that done. I really hope it will help others!
Here is how I'm using my class:
#implementation ViewController
{
SliceView *sv_;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.grayColor;
CGFloat width = 240.0;
CGFloat height = 160.0;
CGRect r = CGRectMake(
self.view.frame.size.width/2 - width/2,
self.view.frame.size.height/2 - height/2,
width, height);
sv_ = [[SliceView alloc] initWithFrame:r fromAngle:150 toAngle:30 color:[UIColor yellowColor] ];
[self.view addSubview:sv_];
}
- (IBAction)pressedGo:(id)sender {
[sv_ animate];
}
I'm adding a slight improvement for David's class. (David - you are welcome to copy into your book-quality answer!)
You can add the following init function:
convenience init(frame:CGRect, startAngle:CGFloat, endAngle:CGFloat, fillColor:UIColor,
strokeColor:UIColor, strokeWidth:CGFloat) {
self.init()
self.frame = frame
self.startAngle = startAngle
self.endAngle = endAngle
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
}
and then call it like this (Objective-C in my case):
PieSegmentLayer *sliceLayer = [[PieSegmentLayer alloc] initWithFrame:r startAngle:30 endAngle:180 fillColor:[UIColor cyanColor] strokeColor:[UIColor redColor] strokeWidth:4];
[self.view.layer addSublayer:sliceLayer];

How can I move image only in limited area?

is there any way to limit the area of moving object?
More precisely i want that the white circle only moves around the green circle.[Screen Bellow] (The same like in the Clue app for tracking woman
Period cycle) Is there any way to do that? I was thinking about circle radius but I can not think of a way to determine the center of that circle.
import UIKit
class MainViewController: UIViewController {
//OUTLETS
#IBOutlet weak var movingCircle: UIImageView!
#IBAction func touchMovingCircle(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: self.view)
if let view = sender.view{
view.center = CGPoint(x:view.center.x + translation.x, y: view.center.y + translation.y)
}
let point = CGPoint(x: 0, y:0)
sender.setTranslation(point, in: self.view)
}
}
I would suggest trying to alter the white circle's anchor point such that it is in the center of the larger circle. Then you could simply rotate the circle and it would give the result you desire.
//Get the necessary y anchor point
let yAnchor = largeCircleRadius / movingCircleDiameter
movingCircle.layer = CGPoint(x: 0.5, y: yAnchor)
That may not be right, but it would be something like that. Then to move it around the circle, all you would have to do is set the rotation of the white circle.
movingCircle.transform = CGAffineTransform(rotationAngle: yourAngle)
You might have to mess with the values, but I've done something similar before.

Resources