Have a fixed hinge while using CGAffineTransform - swift

I'm using CGAffineTransform to rotate a collectionView cell. The rotation hinge is located in the bottom-left. As you can see in the images, here is how it looks
The moving progress
The issue is, I want it to rotate exactly on bottom-left`, like a fixed hinge, but now its corner is moved a bit while it's rotating.
here is my code
private func setupGestures() {
let swipeToLeft = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeLeft))
swipeToLeft.direction = .left
container.addGestureRecognizer(swipeToLeft)
}
#objc func respondToSwipeLeft(gesture: UIGestureRecognizer) {
UIView.animate(withDuration: 0.6, animations: { [weak self] in
guard let this = self else { return }
this.backgroundView.backgroundColor = UIColor.systemRed
this.container.setAnchorPoint(anchorPoint: .init(x: 0, y: 1))
this.container.transform = CGAffineTransform(rotationAngle: -30.degreesToRadians)
haptic.impactOccurred()
}, completion: { [weak self] _ in
guard let this = self else { return }
this.rotateContainerToInitialPosition()
})
}
private func rotateContainerToInitialPosition() {
UIView.animate(withDuration: 0.6) {
self.container.transform = .identity
self.container.setAnchorPoint(anchorPoint: .init(x: 0.5, y: 0.5))
self.backgroundView.backgroundColor = UIColor.background
}
}
and
extension UIView{
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position : CGPoint = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x;
position.y -= oldPoint.y;
position.y += newPoint.y;
self.layer.position = position;
self.layer.anchorPoint = anchorPoint;
}
}
Your help and suggestions will be greatly appreciated.

Related

Subviews are not moving with parent UIView

I'm trying to make an app based on image editing and I'm founding some problems coding a resizing by corners dragging crop area.
I have croppingView.swift where the crop rectangle is drawn when launched:
override func viewDidAppear(_ animated: Bool) {
let imagePoint = CGPoint(x: contentView.center.x, y: contentView.center.y)
let imageSize = CGSize(width: cropImageView.image!.size.width, height: cropImageView.image!.size.height)
let widthScale = cropImageView.frame.width / imageSize.width
let heightScale = cropImageView.frame.height / imageSize.height
let scale = min(widthScale, heightScale)
let width = imageSize.width * scale
let height = imageSize.height * scale
let size = CGSize(width: width, height: height)
let originViewFrame = CGPoint(x: imagePoint.x - width/2.0, y: imagePoint.y - height/2.0)
let cropSize = CGSize(width: width * 0.7, height: height * 0.7)
let cropViewFrame = CGRect(origin: originViewFrame, size: cropSize)
cropView = ShapeView(frame: cropViewFrame)
cropView.backgroundColor = UIColor(red: 224.0/255.0, green: 224.0/255.0, blue: 224.0/255.0, alpha: 0.3)
cropView.layer.borderColor = UIColor(red: 97.0/255.0, green: 97.0/255.0, blue: 97.0/255.0, alpha: 1.0).cgColor
cropView.layer.borderWidth = 1.0
cropView.center = imagePoint
self.view.addSubview(cropView)
}
The ShapeView.swift to draw the cropping area is as follows:
class ShapeView: UIView {
var previousLocation = CGPoint.zero
var topLeft = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var topRight = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var bottomLeft = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var bottomRight = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
override func didMoveToSuperview() {
superview?.addSubview(topLeft)
superview?.addSubview(topRight)
superview?.addSubview(bottomLeft)
superview?.addSubview(bottomRight)
var pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
topLeft.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
topRight.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
bottomLeft.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
bottomRight.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handleMove))
self.addGestureRecognizer(pan)
self.updateDragHandles()
}
func updateDragHandles() {
topLeft.center = self.transformedTopLeft()
topRight.center = self.transformedTopRight()
bottomLeft.center = self.transformedBottomLeft()
bottomRight.center = self.transformedBottomRight()
}
//Gesture Methods
#IBAction func handleMove(gesture:UIPanGestureRecognizer) {
let translation = gesture.translation(in: self.superview!)
var center = self.center
center.x += translation.x
center.y += translation.y
self.center = center
gesture.setTranslation(CGPoint.zero, in: self.superview!)
updateDragHandles()
}
#IBAction func handlePan(gesture:UIPanGestureRecognizer) {
let translation = gesture.translation(in: self)
switch gesture.view! {
case topLeft:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 1))
}
self.bounds.size.width -= translation.x
self.bounds.size.height -= translation.y
case topRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0, y: 1))
}
self.bounds.size.width += translation.x
self.bounds.size.height -= translation.y
case bottomLeft:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 0))
}
self.bounds.size.width -= translation.x
self.bounds.size.height += translation.y
case bottomRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint.zero)
}
self.bounds.size.width += translation.x
self.bounds.size.height += translation.y
default:()
}
gesture.setTranslation(CGPoint.zero, in: self)
updateDragHandles()
if gesture.state == .ended {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
}
}
}
As you can see I'm adding one circle in each corner of the rectangle. These circles are used to resize the cropping area by dragging. Here the key is the updateDragHandles()function which updates the positions of all other circle keeping them on the corners.
So when launching the cropping area appears like:
And when user moves for example the bottom-right corner by dragging everything is working fine:
Now the problem is when I click on the portrait button to change cropping area orientation. With the next code into croppingView.swift the grey rectangle correctly changes the size but the circles remain on the their position:
#IBAction func portButton(_ sender: Any) {
portButton.tintColor = UIColor.systemBlue
landButton.tintColor = UIColor.systemGray
isPortait = 1
let inWidth = cropView.frame.size.width
let inHeight = cropView.frame.size.height
if inWidth > inHeight {
let scaleW = cropView.frame.size.height / cropView.frame.size.width
let scaleH = cropView.frame.size.width / cropView.frame.size.height
let fixPoint = CGPoint(x: 0.5, y: 0.5)
cropView.setAnchorPoint(anchorPoint: fixPoint)
cropView.transform = cropView.transform.scaledBy(x: scaleW, y: scaleH)
let vc = ShapeView()
vc.updateDragHandles()
}
}
It's like updateDragHandles is not working from this ViewController...
Probably it's a basic mistake but I can't find out the solution.
Any suggestion?
Thank you in advance!
DragHandle.swift
let diameter:CGFloat = 30
import UIKit
class DragHandle: UIView {
var fillColor = UIColor.darkGray
var strokeColor = UIColor.lightGray
var strokeWidth: CGFloat = 1.0
required init(coder aDecoder: NSCoder) {
fatalError("Use init(fillColor:, strokeColor:)")
}
init(fillColor: UIColor, strokeColor: UIColor, strokeWidth width: CGFloat = 1.0) {
super.init(frame: CGRect(x: 0, y: 0, width: diameter, height: diameter))
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = width
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect)
{
super.draw(rect)
let handlePath = UIBezierPath(ovalIn: rect.insetBy(dx: 10 + strokeWidth, dy: 10 + strokeWidth))
fillColor.setFill()
handlePath.fill()
strokeColor.setStroke()
handlePath.lineWidth = strokeWidth
handlePath.stroke()
}
}
UIViewExtensions.swift
import Foundation
import UIKit
extension UIView {
func offsetPointToParentCoordinates(point: CGPoint) -> CGPoint {
return CGPoint(x: point.x + self.center.x, y: point.y + self.center.y)
}
func pointInViewCenterTerms(point:CGPoint) -> CGPoint {
return CGPoint(x: point.x - self.center.x, y: point.y - self.center.y)
}
func pointInTransformedView(point: CGPoint) -> CGPoint {
let offsetItem = self.pointInViewCenterTerms(point: point)
let updatedItem = offsetItem.applying(self.transform)
let finalItem = self.offsetPointToParentCoordinates(point: updatedItem)
return finalItem
}
func originalFrame() -> CGRect {
let currentTransform = self.transform
self.transform = .identity
let originalFrame = self.frame
self.transform = currentTransform
return originalFrame
}
func transformedTopLeft() -> CGPoint {
let frame = self.originalFrame()
let point = frame.origin
return self.pointInTransformedView(point: point)
}
func transformedTopRight() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.x += frame.size.width
return self.pointInTransformedView(point: point)
}
func transformedBottomRight() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.x += frame.size.width
point.y += frame.size.height
return self.pointInTransformedView(point: point)
}
func transformedBottomLeft() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.y += frame.size.height
return self.pointInTransformedView(point: point)
}
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
self.layer.position = position
self.layer.anchorPoint = anchorPoint
}
}

How to animate CAGradientLayer color points?

I have a problem while animating the CAGradientLayer angle.
Angle in CAGradientLayer is represented through start and end point properties.
I want to animate the gradient in a circular fashion.
When I set it inside an animationGroup it doesn't work. No animation is happening.
When I am changing the properties in
DispatchQueue.main.asyncAfter(deadline: now() + 1.0) {
// change properties here
}
it works. But a very fast animation is happening. Which is not good enough.
On the internet the only thing there is is locations and color changes, but no angle change.
Below you can find a Playground project to play with
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
// Gradient layer specification
lazy var gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.withAlphaComponent(0.5).cgColor, UIColor.orange.withAlphaComponent(0.5).cgColor, UIColor.orange.cgColor]
gradientLayer.locations = [0, 0.27, 1]
gradientLayer.frame = view.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
return gradientLayer
}()
func animationMapFunction(points: [(CGPoint, CGPoint)], keyPath: String) -> [CABasicAnimation] {
return points.enumerated().map { (arg) -> CABasicAnimation in
let (offset, element) = arg
let gradientStartPointAnimation = CABasicAnimation(keyPath: keyPath)
gradientStartPointAnimation.fromValue = element.0
gradientStartPointAnimation.toValue = element.1
gradientStartPointAnimation.beginTime = CACurrentMediaTime() + Double(offset)
return gradientStartPointAnimation
}
}
lazy var gradientAnimation: CAAnimation = {
let startPointAnimationPoints = [(CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y:0.0)),
(CGPoint(x: 1.0, y:0.0), CGPoint(x: 1.0, y:1.0)),
(CGPoint(x: 1.0, y:1.0), CGPoint(x: 0.0, y:1.0)),
(CGPoint(x: 0.0, y:1.0), CGPoint(x: 0.0, y:0.0))]
let endPointAnimatiomPoints = [(CGPoint(x: 1.0, y:1.0), CGPoint(x: 0.0, y:1.0)),
(CGPoint(x: 0.0, y:1.0), CGPoint(x: 0.0, y:0.0)),
(CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y:0.0)),
(CGPoint(x: 1.0, y:0.0), CGPoint(x: 1.0, y:1.0))]
let startPointAnimations = animationMapFunction(points: startPointAnimationPoints, keyPath: "startPoint")
let endPointAnimations = animationMapFunction(points: startPointAnimationPoints, keyPath: "endPoint")
let animationGroup = CAAnimationGroup()
animationGroup.duration = 5.0
animationGroup.repeatCount = Float.infinity
animationGroup.animations = startPointAnimations + endPointAnimations
return animationGroup
}()
override func loadView() {
let view = UIView(frame: UIScreen.main.bounds)
self.view = view
view.layer.addSublayer(gradientLayer)
}
func animate() {
view.layer.removeAllAnimations()
gradientLayer.add(gradientAnimation, forKey: nil)
}
}
// Present the view controller in the Live View window
let vc = MyViewController()
PlaygroundPage.current.liveView = vc
vc.animate()
So what I did was a timer, that triggers start and end points change.
I rotate from 0 to 360 degrees, while incrementing the angle with a given constant (in my case 6)
I created a function mapping: angle → (startPoint, endPoint)
func animate() {
stopAnimation()
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { timer in
self.angle += 6.0
self.angle.formTruncatingRemainder(dividingBy: 360)
CATransaction.begin()
// disable implicit animation
CATransaction.setDisableActions(true)
let pos = points(from: self.angle)
self.gradientLayer.startPoint = pos.0
self.gradientLayer.endPoint = pos.1
CATransaction.commit()
})
}
func stopAnimation() {
timer?.invalidate()
}
And here are the utility functions
extension CGPoint {
var inverse: CGPoint {
return CGPoint(x: 1 - x, y: 1 - y)
}
}
fileprivate func points(from angle: Double) -> (CGPoint, CGPoint) {
let start: CGPoint
switch angle {
case let x where 0 <= x && x < 90:
start = CGPoint(x: x / 180, y: 0.5 - x / 180)
case let x where 90 <= x && x < 180:
start = CGPoint(x: x / 180, y: x / 180 - 0.5)
case let x where 180 <= x && x < 270:
start = CGPoint(x: 2.0 - x / 180, y: 0.5 + (x - 180) / 180)
case let x where 270 <= x && x < 360:
start = CGPoint(x: 2.0 - x / 180, y: 0.5 + (360 - x) / 180)
default:
start = CGPoint.zero
}
return (start, start.inverse)
}

Swift: Around of view black

Hello I'm creating a function to crop the photo, I would like the outside of the container that cropping the photo is black with an opacity of 0.5, look at the picture below to better understand:
All I drew in black is what I want it to be black with an opacity of 0.5, how to do this?
The user can move and change the size of frame, I use for this size change CGRect. So it is necessary that the black background is dynamic with the change of the size and the displacement of the frame, I do not know how to do it
The class of my UIIvire frame:
class ResizableView: UIView {
enum Edge {
case topLeft, topRight, bottomLeft, bottomRight, none
}
static var edgeSize: CGFloat = 44.0
private typealias `Self` = ResizableView
var currentEdge: Edge = .none
var touchStart = CGPoint.zero
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
touchStart = touch.location(in: self)
currentEdge = {
if self.bounds.size.width - touchStart.x < Self.edgeSize && self.bounds.size.height - touchStart.y < Self.edgeSize {
return .bottomRight
} else if touchStart.x < Self.edgeSize && touchStart.y < Self.edgeSize {
return .topLeft
} else if self.bounds.size.width - touchStart.x < Self.edgeSize && touchStart.y < Self.edgeSize {
return .topRight
} else if touchStart.x < Self.edgeSize && self.bounds.size.height - touchStart.y < Self.edgeSize {
return .bottomLeft
}
return .none
}()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let currentPoint = touch.location(in: self)
let previous = touch.previousLocation(in: self)
let originX = self.frame.origin.x
let originY = self.frame.origin.y
let width = self.frame.size.width
let height = self.frame.size.height
let deltaWidth = currentPoint.x - previous.x
let deltaHeight = currentPoint.y - previous.y
switch currentEdge {
case .topLeft:
self.frame = CGRect(x: originX + deltaWidth, y: originY + deltaHeight, width: width - deltaWidth, height: height - deltaHeight)
case .topRight:
self.frame = CGRect(x: originX, y: originY + deltaHeight, width: width + deltaWidth, height: height - deltaHeight)
case .bottomRight:
self.frame = CGRect(x: originX, y: originY, width: currentPoint.x + deltaWidth, height: currentPoint.y + deltaWidth)
case .bottomLeft:
self.frame = CGRect(x: originX + deltaWidth, y: originY, width: width - deltaWidth, height: height + deltaHeight)
default:
// Moving
self.center = CGPoint(x: self.center.x + currentPoint.x - touchStart.x,
y: self.center.y + currentPoint.y - touchStart.y)
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
currentEdge = .none
}
// MARK: - Border
#IBInspectable public var borderColor: UIColor = UIColor.clear {
didSet {
layer.borderColor = borderColor.cgColor
}
}
#IBInspectable public var borderWidth: CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
}
Use the maskView to insert an image which covers the bleed area. The image should be 50% opacity in the bleed area. Here's an example on how to implement this technique.
let imageView = UIImageView(image: UIImage(named: "star"))
imageView.contentMode = .scaleAspectFill
view.maskView = imageView

Swift draw infinite sine curve (UIBezierpath)

class GraphView: UIView {
private let axesDrawer = AxesDrawer()
private var scale: CGFloat = 1.0 { didSet { setNeedsDisplay() } }
private var axesCenter: CGPoint? { didSet { setNeedsDisplay() } }
private var positionX: CGFloat = 0 { didSet { setNeedsDisplay() } }
func changeScale(recognizer: UIPinchGestureRecognizer) {
switch recognizer.state {
case .Changed, .Ended:
scale *= recognizer.scale
recognizer.scale = 1.0
default: break
}
}
func moveAxes(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .Changed:
if axesCenter == nil {
axesCenter = CGPoint(x: self.frame.midX, y: self.frame.midY)
}
let changePoint = recognizer.translationInView(self)
axesCenter = CGPoint(x: axesCenter!.x + changePoint.x, y: axesCenter!.y + changePoint.y)
positionX += changePoint.x
recognizer.setTranslation(CGPoint(x: 0.0, y: 0.0), inView: self)
default:
break
}
}
func drawGraph() -> UIBezierPath {
let path = UIBezierPath()
let positionY = (axesCenter == nil) ? frame.height / 2 : (axesCenter?.y)! // find the vertical center
path.moveToPoint(CGPoint(x: self.frame.minX, y: positionY))
for i in Int(frame.minX)...Int(frame.maxX) {
let x = CGFloat(i) + positionX
let y = (sin(Double(i) * 0.1) * Double(axesDrawer.minimumPointsPerHashmark) * Double(scale)) + Double(positionY)
path.addLineToPoint(CGPoint(x: x, y: CGFloat(y)))
}
path.lineWidth = 1
return path
}
override func drawRect(rect: CGRect) {
axesDrawer.contentScaleFactor = self.contentScaleFactor
axesDrawer.drawAxesInRect(self.bounds, origin: axesCenter ?? CGPoint(x: self.frame.midX, y: self.frame.midY), pointsPerUnit: CGFloat(50) * scale)
UIColor.redColor().set()
drawGraph().stroke()
}
}
Moving towards:
left side
right side
I'm trying to draw "infinite" sine curve. The problem is that it doesn't redraw itself when I try to move around the view.
If you look at the links I provided, on the left side an infinite line appears, and If you go right the curve just stops.
How can I update UI for the curve to be redrawn every time I try to move around the view?

how to replace panGestureDidMove with fixed start to end position

I have been using UIPanGestureRecognizer to recognise touches but I want to replace it with a fixed start to end position for my animation. Please see the code below:
panGestureDidMove:
func panGestureDidMove(gesture: UIPanGestureRecognizer) {
if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled {
} else {
let additionalHeight = max(gesture.translationInView(view).y, 0)
let waveHeight = min(additionalHeight * 0.6, maxWaveHeight)
let baseHeight = minimalHeight + additionalHeight - waveHeight
let locationX = gesture.locationInView(gesture.view).x
layoutControlPoints(baseHeight: baseHeight, waveHeight: waveHeight, locationX: locationX)
updateShapeLayer()
}
}
layoutControlPoints:
private func layoutControlPoints(baseHeight baseHeight: CGFloat, waveHeight: CGFloat, locationX: CGFloat) {
let width = view.bounds.width
let minLeftX = min((locationX - width / 2.0) * 0.28, 0.0)
let maxRightX = max(width + (locationX - width / 2.0) * 0.28, width)
let leftPartWidth = locationX - minLeftX
let rightPartWidth = maxRightX - locationX
l3ControlPointView.center = CGPoint(x: minLeftX, y: baseHeight)
l2ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.44, y: baseHeight)
l1ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
cControlPointView.center = CGPoint(x: locationX , y: baseHeight + waveHeight * 1.36)
r1ControlPointView.center = CGPoint(x: maxRightX - rightPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
r2ControlPointView.center = CGPoint(x: maxRightX - (rightPartWidth * 0.44), y: baseHeight)
r3ControlPointView.center = CGPoint(x: maxRightX, y: baseHeight)
}
I am trying to replace the panGestureDidMove with CABasicAnimation to animate the start to end position, something like the code below:
let startValue = CGPointMake(70.0, 50.0)
let endValue = CGPointMake(90.0, 150.0)
CATransaction.setDisableActions(true) //Not necessary
view.layer.bounds.size.height = endValue
let positionAnimation = CABasicAnimation(keyPath:"bounds.size.height")
positionAnimation.fromValue = startValue
positionAnimation.toValue = endValue
positionAnimation.duration = 2.0
view.layer.addAnimation(positionAnimation, forKey: "bounds")
A lot of things are affected as the position changes, how can I achieve this?
If you want fine control of animation, you can use CADisplayLink to do any custom layout or drawing prior to the screens pixel refresh. The run loop attempts to draw 60 frames per second, so with that in mind you can modify your code to simulate touch events.
You'll need to add some properties:
var displayLink:CADisplayLink? // let's us tap into the drawing run loop
var startTime:NSDate? // while let us keep track of how long we've been animating
var deltaX:CGFloat = 0.0 // how much we should update in the x direction between frames
var deltaY:CGFloat = 0.0 // same but in the y direction
var startValue = CGPointMake(70.0, 50.0) // where we want our touch simulation to start
var currentPoint = CGPoint(x:0.0, y:0.0)
let endValue = CGPointMake(90.0, 150.0) // where we want our touch simulation to end
Then whenever we want the animation to run we can call:
func animate()
{
let duration:CGFloat = 2.0
self.currentPoint = self.startValue
self.deltaX = (endValue.x - startValue.x) / (duration * 60.0) // 60 frames per second so getting the difference then dividing by the duration in seconds times 60
self.deltaY = (endValue.y - startValue.y) / (duration * 60.0)
self.startTime = NSDate()
self.displayLink = CADisplayLink(target: self, selector: #selector(self.performAnimation))
self.displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
}
This will set up the display link and determine my how much our touch simulation should move between frames and begin calling the function that will be called prior to every drawing of the screen performAnimation:
func performAnimation(){
if self.startTime?.timeIntervalSinceNow > -2 {
self.updateViewsFor(self.currentPoint, translation: CGPoint(x:self.currentPoint.x - self.startValue.x, y: self.currentPoint.y - self.startValue.y))
self.currentPoint.x += self.deltaX
self.currentPoint.y += self.deltaY
}
else
{
self.displayLink?.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
self.currentPoint = self.startValue
}
}
Here we check if we are within the duration of the animation. Then call updateViewsFor(point:CGPoint,translation:CGPoint) which is what you had in your gesture target and then update our touch simulation if we're within it, else we just reset our properties.
Finally,
func updateViewsFor(point:CGPoint,translation:CGPoint)
{
let additionalHeight = max(translation.y, 0)
let waveHeight = min(additionalHeight * 0.6, maxWaveHeight)
let baseHeight = minimalHeight + additionalHeight - waveHeight
let locationX = point.x
layoutControlPoints(baseHeight: baseHeight, waveHeight: waveHeight, locationX: locationX)
updateShapeLayer()
}
You could also change your panGestureDidMove to:
#objc func panGestureDidMove(gesture: UIPanGestureRecognizer) {
if gesture.state == .Ended || gesture.state == .Failed || gesture.state == .Cancelled {
} else {
self.updateViewsFor(gesture.locationInView(gesture.view), translation: gesture.translationInView(view))
}
}
Edit
There is an easier way of doing this using keyframe animation. But I'm not sure it will animate your updateShapeLayer(). But for animating views we can write a function like:
func animate(fromPoint:CGPoint, toPoint:CGPoint, duration:NSTimeInterval)
{
// Essentually simulates the beginning of a touch event at our start point and the touch hasn't moved so tranlation is zero
self.updateViewsFor(fromPoint, translation: CGPointZero)
// Create our keyframe animation using UIView animation blocks
UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: .CalculationModeLinear, animations: {
// We really only have one keyframe which is the end. We want everything in between animated.
// So start time zero and relativeDuration 1.0 because we want it to be 100% of the animation
UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1.0, animations: {
// we want our "touch" to move to the endValue and the translation will just be the difference between end point and start point in the x and y direction.
self.updateViewsFor(toPoint, translation: CGPoint(x: toPoint.x - fromPoint.x, y: toPoint.y - fromPoint.y))
})
}, completion: { _ in
// do anything you need done after the animation
})
}
This will move the views into place then create a keyframe for where the views end up and animate everything in between. We could call it like:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.animate(CGPointMake(70.0, 50.0), toPoint: CGPointMake(90.0, 150.0), duration: 2.0)
}