Let's say I have an animator that moves a view from (0, 0) to (-120, 0):
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.8)
animator.addAnimations {
switch state:
case .normal: view.frame.origin.x = 0
case .swiped: view.frame.origin.x = -120
}
}
I use it together with UIPanGestureRecognizer, so that I can resize the view continuously along with the finger movements.
The issue comes when I want to add some sort of bouncing effect at the start or at the end of the animation. NOT just the damping ratio, but the bounce effect. The easiest way to imagine this is Swipe-To-Delete feature of UITableViewCell, where you can drag "Delete" button beyond its actual width, and then it bounces back.
Effectively what I want to achieve, is the way to set fractionComplete property outside of [0, 1] segment, so when the fraction is 1.2, the offset becomes 144 instead of its 120 maximum.
And right now the maximum value for fractionComplete is exactly 1.
Below are some examples to have this issue visualized:
What I currently have:
What I want to achieve:
EDIT (19 January):
Sorry for my delayed reply. Here are some clarifications:
I don't use UIView.animate(...), and use UIViewPropertyAnimator instead for a very specific reason: it handles for me all the timings, curves and velocities.
For example, you dragged the view halfway through. This means that duration of the remaining part should be two times less than total duration. Or if you dragged though the 99% of the distance, it should complete the remaining part almost instantly.
As an addition, UIViewPropertyAnimator has such features as pause (when user starts dragging once again), or reverse (when user started dragging to the left, but after that he changed his mind and moved the finger to the right), that I also benefit from.
All this is not available for simple UIView animations, or requires TONS of effort at best. It is only capable of simple transitions, and this is not the case.
That's why I have to use some sort of animator.
And as I mentioned in the comments thread in the answer that was removed by its publisher, the most complex part for me here is to simulate the friction effect: the further you drag, the less the view actually moves. Just as when you're trying to drag any UIScrollView outside of it's content.
Thanks for your effort guys, but I don't think any of these 2 answers is relevant. I will try to implement this behaviour using UIDynamicAnimator whenever I have time. Probably in the nearest week or two. I will publish my approach in case I have any decent results.
EDIT (20 January):
I just uploaded a demo project to the GitHub, which includes all the transitions that I have in my project. So now you can actually have an idea why do I need to use animators and how I use them: https://github.com/demon9733/bouncingview-prototype
The only file you are actually interested in is MainViewController+Modes.swift. Everything related to transitions and animations is contained there.
What I need to do is to enable user to drag the handle area beyond "Hide" button width with a damping effect. "Hide" button will appear on swiping the handle area to the left.
P.S. I didn't really test this demo, so it can have bugs that I don't have in my main project. So you can safely ignore them.
you need to allow pan gesture to get to needed x position and at the end of pan an animation is needed to be triggered
one way to do this would be:
var initial = CGRect.zero
override func viewDidLayoutSubviews() {
initial = animatedView.frame
}
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let closed = initial
let open = initial.offsetBy(dx: -120, dy: 0)
// 1 manage panning along x direction
sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x, y: (sender.view?.center.y)! )
sender.setTranslation(CGPoint.zero, in: self.view)
// 2 animate to needed position once pan ends
if sender.state == .ended {
if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
UIView.animate(withDuration: 1 , animations: {
sender.view?.frame = closed
})
} else {
UIView.animate(withDuration: 1 , animations: {
sender.view?.frame = open
})
}
}
}
Edit 20 Jan
For simulating dampening effect and make use of UIViewPropertyAnimator specifically,
var initialOrigin = CGRect.zero
override func viewDidLayoutSubviews() {
initialOrigin = animatedView.frame
}
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let closed = initialOrigin
let open = initialOrigin.offsetBy(dx: -120, dy: 0)
// 1. to simulate dampening
var multiplier: CGFloat = 1.0
if animatedView?.frame.origin.x ?? CGFloat(0) > closed.origin.x || animatedView?.frame.origin.x ?? CGFloat(0) < open.origin.x {
multiplier = 0.2
} else {
multiplier = 1
}
// 2. animate panning
sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x * multiplier, y: (sender.view?.center.y)! )
sender.setTranslation(CGPoint.zero, in: self.view)
// 3. animate to needed position once pan ends
if sender.state == .ended {
if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
self.animatedView.frame.origin.x = closed.origin.x
})
animate.startAnimation()
} else {
let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
self.animatedView.frame.origin.x = open.origin.x
})
animate.startAnimation()
}
}
}
Here is possible approach (simplified & a bit scratchy - only bounce, w/o button at right, because it would much more code and actually only a matter of frames management)
Due to long delay of UIPanGestureRecognizer at ending, I prefer to use UILongPressGestureRecognizer, as it gives faster feedback.
Here is demo result
The Storyboard of used below ViewController has only gray-background-rect-container view, everything else is done in code provided below.
class ViewController: UIViewController {
#IBOutlet weak var container: UIView!
let imageView = UIImageView()
var initial: CGFloat = .zero
var dropped = false
private func excedesLimit() -> Bool {
// < set here desired bounce limits
return imageView.frame.minX < -180 || imageView.frame.minX > 80
}
#IBAction func pressHandler(_ sender: UILongPressGestureRecognizer) {
let location = sender.location(in: imageView.superview).x
if sender.state == .began {
dropped = false
initial = location - imageView.center.x
}
else if !dropped {
if (sender.state == .changed) {
imageView.center = CGPoint(x: location - initial, y: imageView.center.y)
dropped = excedesLimit()
}
if sender.state == .ended || dropped {
initial = .zero
// variant with animator
let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
let stickTo: CGFloat = self.imageView.frame.minX < -100 ? -100 : 0 // place for button at right
self.imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: self.imageView.frame.origin.y), size: self.imageView.frame.size)
}
animator.isInterruptible = true
animator.startAnimation()
// uncomment below - variant with UIViewAnimation
// UIView.beginAnimations("bounce", context: nil)
// UIView.setAnimationDuration(0.2)
// UIView.setAnimationTransition(.none, for: imageView, cache: true)
// UIView.setAnimationBeginsFromCurrentState(true)
//
// let stickTo: CGFloat = imageView.frame.minX < -100 ? -100 : 0 // place for button at right
// imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: imageView.frame.origin.y), size: imageView.frame.size)
// UIView.setAnimationDelegate(self)
// UIView.setAnimationDidStop(#selector(makeBounce))
// UIView.commitAnimations()
}
}
}
// #objc func makeBounce() {
// let bounceAnimation = CABasicAnimation(keyPath: "position.x")
// bounceAnimation.duration = 0.1
// bounceAnimation.repeatCount = 0
// bounceAnimation.autoreverses = true
// bounceAnimation.fillMode = kCAFillModeBackwards
// bounceAnimation.isRemovedOnCompletion = true
// bounceAnimation.isAdditive = false
// bounceAnimation.timingFunction = CAMediaTimingFunction(name: "easeOut")
// imageView.layer.add(bounceAnimation, forKey:"bounceAnimation");
// }
override func viewDidLoad() {
super.viewDidLoad()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = UIImage(named: "cat")
imageView.contentMode = .scaleAspectFill
imageView.layer.borderColor = UIColor.red.cgColor
imageView.layer.borderWidth = 1.0
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true
container.addSubview(imageView)
imageView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: container.centerYAnchor).isActive = true
imageView.widthAnchor.constraint(equalTo: container.widthAnchor, multiplier: 1).isActive = true
imageView.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 1).isActive = true
let pressGesture = UILongPressGestureRecognizer(target: self, action: #selector(pressHandler(_:)))
pressGesture.minimumPressDuration = 0
pressGesture.allowableMovement = .infinity
imageView.addGestureRecognizer(pressGesture)
}
}
Related
I'm trying to add a swiping future to a view, but I want it to move by moving the finger tip on it, around a corner of the view and when it reach a certain point, it stop moving.
I did it with UISwipeGestureRecognizerbut it moves automatically with animation, also the it's not exactly rotating around corner
let swipeToRight = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeRight))
swipeToRight.direction = .right
container.addGestureRecognizer(swipeToRight)
#objc func respondToSwipeRight(gesture: UIGestureRecognizer) {
UIView.animate(withDuration: 0.6, animations: { [weak self] in
guard let this = self else { return }
this.backgroundView.backgroundColor = UIColor.systemGreen
this.container.setAnchorPoint(anchorPoint: .init(x: 1, y: 1))
this.container.transform = CGAffineTransform(rotationAngle: 15.degreesToRadians)
haptic.impactOccurred()
}, completion: { [weak self] _ in
guard let this = self else { return }
this.rotateContainerToInitialPosition()
})
}
I want to do it with pan gestures or something similar that it let the view to move exactly with finger, and come back when it's released , but I don't know how to do it. could anyone show me how? Thank you so much
Here's an example of using a UIPanGestureRecognizer to rotate the box as the user moves their finger right and left. Be sure to start your pan gesture on the box.
Start with a new iOS app project. Use the following for the ViewController class. No other changes to the project are needed. Run on your favorite iOS device or simulator.
class ViewController: UIViewController {
var box: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let container = UIView(frame: .zero)
container.backgroundColor = .systemBlue
container.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(container)
// Just some example constraints to put the box on the screen
NSLayoutConstraint.activate([
container.heightAnchor.constraint(equalToConstant: 250),
container.widthAnchor.constraint(equalTo: container.heightAnchor),
container.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30),
container.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
])
// The box that will be rotated
// Sized to match the blue container
box = UIView(frame: container.bounds)
box.backgroundColor = .systemGreen
box.autoresizingMask = [.flexibleWidth, .flexibleHeight]
box.anchorPoint = CGPoint(x: 1, y: 1) // bottom-right corner for rotation
container.addSubview(box)
// Setup the pan gesture and add to the blue container
let pan = UIPanGestureRecognizer(target: self, action: #selector(panHandler))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
container.addGestureRecognizer(pan)
}
#objc func panHandler(_ gesture: UIPanGestureRecognizer) {
if gesture.state == .began || gesture.state == .changed {
// Relative offset from the start of the gesture
let offset = gesture.translation(in: gesture.view)
// Ignore moves to the left of the starting point
let right = max(offset.x, 0)
// Only rotate up to 75 degrees - just an example
let angleDeg = min(right / box.bounds.size.width * 90, 75)
// Create and set the rotation transform on the green box
let rotate = CGAffineTransform(rotationAngle: angleDeg / 180 * .pi)
box.transform = rotate
}
}
}
Start a pan in the box - move right and left and the green box rotates back and forth as the finger moves. Start a new pan and the rotation resets.
I'm having a container view and added a UIPanGestureRecognizer on it. It works like this image:
With the UIPanGestureRecognizer it's possible to swipe to the right, and it works great
#objc func respondToSwipeRight(recognizer: UIPanGestureRecognizer) {
container.setAnchorPoint(anchorPoint: .init(x: 1, y: 1))
let touchLocation = recognizer.location(in: container.superview)
let center = container.center
switch recognizer.state{
case .began :
self.deltaAngle = atan2(touchLocation.y - center.y, touchLocation.x - center.x) - atan2(container.transform.b, container.transform.a)
case .changed:
backgroundView.backgroundColor = UIColor.systemGreen
let angle = atan2(touchLocation.y - center.y, touchLocation.x - center.x)
let angleDiff = self.deltaAngle - angle
if angleDiff <= 0, angleDiff > -5, angleDiff > -0.50 {
container.transform = CGAffineTransform(rotationAngle: -angleDiff)
}
case .ended:
UIView.animate(withDuration: 0.6, animations: { [weak self] in
guard let this = self else { return }
this.container.transform = CGAffineTransform(rotationAngle: 0)
}, completion: { [weak self] _ in
guard let this = self else { return }
this.container.setAnchorPoint(anchorPoint: .init(x: 0.5, y: 0.5))
})
default: break
}
}
Now, I want to have the same things from right to left, I added a new UIPanGestureRecognizer to that view, but appearance I only can have one UIPanGestureRecognizer per view and it uses the latest one.
Could anyone help me to have the same mechanism for right to left? For right to left, the container anchor point should be like that:
container.setAnchorPoint(anchorPoint: .init(x: 0, y: 1))
Your help will be appreciated
Here's an example of using a single UIPanGestureRecognizer to rotate the box around either the bottom-left or bottom-right corner as the user moves their finger right and left. Be sure to start your pan gesture on the box.
Start with a new iOS app project. Use the following for the ViewController class. No other changes to the project are needed. Run on your favorite iOS device or simulator.
enum PanDirection {
case unknown
case left
case right
}
class ViewController: UIViewController {
var box: UIView!
var direction: PanDirection = .unknown
override func viewDidLoad() {
super.viewDidLoad()
let container = UIView(frame: .zero)
container.backgroundColor = .systemBlue
container.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(container)
// Just some example constraints to put the box on the screen
NSLayoutConstraint.activate([
container.heightAnchor.constraint(equalToConstant: 250),
container.widthAnchor.constraint(equalTo: container.heightAnchor),
container.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30),
container.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
])
// The box that will be rotated
// Sized to match the blue container
box = UIView(frame: container.bounds)
box.backgroundColor = .systemGreen
box.autoresizingMask = [.flexibleWidth, .flexibleHeight]
container.addSubview(box)
// Setup the pan gesture and add to the blue container
let pan = UIPanGestureRecognizer(target: self, action: #selector(panHandler))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
container.addGestureRecognizer(pan)
}
#objc func panHandler(_ gesture: UIPanGestureRecognizer) {
if gesture.state == .began || gesture.state == .changed {
// Relative offset from the start of the gesture
let offset = gesture.translation(in: gesture.view)
// Determine which direction we are panning
if offset.x == 0 {
direction = .unknown
} else {
direction = offset.x > 0 ? .right : .left
}
if direction == .right {
// How far right we have panned
let right = offset.x
// Only rotate up to 75 degrees - just an example
let angleDeg = min(right / box.bounds.size.width * 90, 75)
// Calculate the transform. We need to translate the box to the bottom-right corner,
// then rotate, then translate back to the center.
let shift = CGAffineTransform(translationX: box.bounds.size.width / 2, y: box.bounds.size.height / 2)
box.transform = shift.rotated(by: angleDeg / 180 * .pi).translatedBy(x: -box.bounds.size.width / 2, y: -box.bounds.size.height / 2)
} else if direction == .left {
// How far to the left we have panned
let left = offset.x
// Only rotate up to 75 degrees - just an example
let angleDeg = max(left / box.bounds.size.width * 90, -75)
// Calculate the transform. We need to translate the box to the bottom-left corner,
// then rotate, then translate back to the center.
let shift = CGAffineTransform(translationX: -box.bounds.size.width / 2, y: box.bounds.size.height / 2)
box.transform = shift.rotated(by: angleDeg / 180 * .pi).translatedBy(x: box.bounds.size.width / 2, y: -box.bounds.size.height / 2)
}
} else {
// Reset for next pan gesture
direction = .unknown
}
}
}
Start a pan in the box - move right and left and the green box rotates back and forth around the appropriate corner as the finger moves.
I have a rotateArt function that rotates a UIImage - my problem is when this function is called following a pan gesture the image snaps back to the views centre. I would like it to rotate around its current centre.
I have tried assigning a variable to hold its current centre but that did not work. What am I missing?
Thanks
extension MainViewController {
func imageCheck() {
pickedImage != nil ? addPainting() : print("image did not load")
}
func addPainting() {
pickedImageView = UIImageView(image: pickedImage)
pickedImageView.isUserInteractionEnabled = true
pickedImageView.contentMode = .scaleAspectFit
view.addSubview(pickedImageView)
pickedImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pickedImageView.centerXAnchor.constraint(equalTo: userPickedRoom.centerXAnchor),
pickedImageView.centerYAnchor.constraint(equalTo: userPickedRoom.centerYAnchor, constant: -50),
pickedImageView.widthAnchor.constraint(equalToConstant: 150),
pickedImageView.heightAnchor.constraint(equalToConstant: 150)
])
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
pickedImageView.addGestureRecognizer(panGesture)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch))
view.addGestureRecognizer(pinchGesture)
addShadow()
}
#objc func handlePan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
guard let gestureView = sender.view else {return}
gestureView.center = CGPoint(x: gestureView.center.x + translation.x, y: gestureView.center.y + translation.y)
sender.setTranslation(.zero, in: view)
}
#objc func handlePinch(_ sender: UIPinchGestureRecognizer) {
guard sender.view != nil else { return }
if sender.state == .began || sender.state == .changed {
pickedImageView.transform = (pickedImageView.transform.scaledBy(x: sender.scale, y: sender.scale))
sender.scale = 1.0
}
}
func rotateArt() { // pickedImageView.center returns to view.center when this functinon is called.
UIImageView.animate(withDuration: 0.1, animations: {
self.pickedImageView.transform = self.pickedImageView.transform.rotated(by: .pi / 2)
})
}
func addShadow() {
pickedImageView.layer.shadowColor = UIColor.black.cgColor
pickedImageView.layer.shadowOpacity = 1
pickedImageView.layer.shadowOffset = CGSize(width: 0.1, height: 0.5)
pickedImageView.layer.shadowRadius = 1
pickedImageView.layer.shouldRasterize = true
pickedImageView.layer.rasterizationScale = UIScreen.main.scale
}
}
The problem is these lines:
pickedImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate
You cannot position pickedImageView using constraints and also animate it by changing its center (as you do during the pan gesture).
Constraints are one thing. Frames/centers are a different thing. They are opposites. You have created a conflict here: you say one thing by changing the center during the pan, but the constraints say something else (they still want to put the image view where you told them to put it).
You need to resolve that conflict. The easiest way is not to use constraints at all, but there are other ways.
So I'm using the new UIViewPropertyAnimator and UIVisualEffectView to achieve the same thing as the Spotlight search when you scrolling down on the home screen and it blurs the background.
I'm using the fractionComplete property to set the procent of how much to blur when panning a UIView.
animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.blurEffectView.effect = nil
}
And the amount of blurriness is changed with a value between 0.0 - 1.0.
animator?.fractionComplete = blurValue
But when I cancel the pan gesture I want the blur to animate back from where it is to no blur (e.g ~ -> 1.0) with a duration of something like 0.4 milliseconds.
Right now I just set the fractionComplete to 1.0 when the pan gesture is cancelled. Instead I want to animate it.
I have tried the UIView.animate(withDuration.. but it doesn't affect the UIViewPropertyAnimators fractionComplete, and thats the only way to blur an UIVisualEffectView.
Any ideas?
It seems that fractionComplete has a bug (my question on Stackoverflow: UIViewPropertyAnimator does not update the view when expected), rdar://30856746. The property only sets the state from inactive to active, but does not update the view, because (I assume) there is another internal state that does not trigger.
To workaround the problem you can do this:
animator.startAnimation() // This will change the `state` from inactive to active
animator.pauseAnimation() // This will change `isRunning` back to false, but the `state` will remain as active
// Now any call of `fractionComplete` should update your view correctly!
animator.fractionComplete = /* your value here */
Here is a playground snippet to play around:
let liveView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 50))
liveView.backgroundColor = .white
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = liveView
let square = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
square.backgroundColor = .red
liveView.addSubview(square)
let animator = UIViewPropertyAnimator.init(duration: 5, curve: .linear)
animator.addAnimations {
square.frame.origin.x = 350
}
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
blurView.frame = liveView.bounds
liveView.addSubview(blurView)
animator.addAnimations {
blurView.effect = nil
}
// If you want to restore the blur after it was animated, you have to
// safe a reference to the effect which is manipulated
let effect = blurView.effect
animator.addCompletion {
// In case you want to restore the blur effect
if $0 == .start { blurView.effect = effect }
}
animator.startAnimation()
animator.pauseAnimation()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
animator.fractionComplete = 0.5
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
// decide the direction you want your animation to go.
// animator.isReversed = true
animator.startAnimation()
}
If you're still looking for a way to actually animate fractionComplete without the use of a slider or a gesture, I was quite happy with my results using a CADisplayLink. You can see my results here: https://gist.github.com/vegather/07993d15c83ffcd5182c8c27f1aa600b
I've used a delayed loop to decrease "fractionComplete"
func resetAnimator(){
let duration = 1.0 / 60.0
if self.animator.fractionComplete > 0 {
self.animator.fractionComplete -= 0.06
delay(duration, closure: {
self.resetAnimator()
})
}
}
func delay(_ delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
SWIFT - OSX
I have a bunch of imageViews set in my Main.storyboard. I am trying to make them spin when the app starts and i would like them to indefinitely. I came across the roateByAngle(angle: CGFloat), but this doesn't animate it, instead it just jumps to the new angle.
I would like to make two functions, spinClockwise() and spinAntiClockwise() so I can just call them in the viewDidLoad and they will just keep turning.
Ive been playing with CATransform3DMakeRotation but cannot seem to get my desired results
let width = myImg.frame.width / 2
let height = myImg.frame.height / 2
myImg.layer?.transform = CATransform3DMakeRotation(180, width, height, 1)
Let me know if i can be more specific.
Thanks
You could add an extension of UIView or UIImageView like this:
extension UIView {
///The less is the timeToRotate, the more fast the animation is !
func spinClockwise(timeToRotate: Double) {
startRotate(CGFloat(M_PI_2), timeToRotate: timeToRotate)
}
///The less is the timeToRotate, the more fast the animation is !
func spinAntiClockwise(timeToRotate: Double) {
startRotate(CGFloat(-M_PI_2), timeToRotate: timeToRotate)
}
func startRotate(angle: CGFloat, timeToRotate: Double) {
UIView.animateWithDuration(timeToRotate, delay: 0.0, options:[UIViewAnimationOptions.CurveLinear, UIViewAnimationOptions.Repeat], animations: {
self.transform = CGAffineTransformMakeRotation(angle)
}, completion: nil)
print("Start rotating")
}
func stopAnimations() {
self.layer.removeAllAnimations()
print("Stop rotating")
}
}
So when you want to rotate your myImg, you just have to call:
myImg.spinClockwise(3)
And when you want to stop it:
myImg.stopAnimations()
NOTE:
I added a playground just so you can test it out ;)
Cheers!
EDIT:
My bad, Here is the example for NSView:
extension NSView {
///The less is the timeToRotate, the more fast the animation is !
func spinClockwise(timeToRotate: Double) {
startRotate(CGFloat(-1 * M_PI * 2.0), timeToRotate: timeToRotate)
}
///The less is the timeToRotate, the more fast the animation is !
func spinAntiClockwise(timeToRotate: Double) {
startRotate(CGFloat(M_PI * 2.0), timeToRotate: timeToRotate)
}
func startRotate(angle: CGFloat, timeToRotate: Double) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = angle
rotateAnimation.duration = timeToRotate
rotateAnimation.repeatCount = .infinity
self.layer?.addAnimation(rotateAnimation, forKey: nil)
Swift.print("Start rotating")
}
func stopAnimations() {
self.layer?.removeAllAnimations()
Swift.print("Stop rotating")
}
}
Important note: Now, after my tests, I noticed that you must set the anchor point of your NSView in the middle so that it can rotate around its center:
view.layer?.anchorPoint = CGPointMake(0.5, 0.5)
I added a new playground with the OSX example
For me, I could not change the anchor point. It was spinning around (0,0) which is bottom left. I moved the anchor point to (0.5, 0.5), but still no luck. Then I came accross with this answer. I modified my code like below, and it begins to rotate around itself. I observed a drawback though, the place of the view somehow shifted, but it can be fixed by trial and error, trying to get it to the right place.
extension NSView {
func startRotating(duration: Double = 1) {
let kAnimationKey = "rotation"
//self.wantsLayer = true
if layer?.animation(forKey: kAnimationKey) == nil {
var oldFrame = self.frame
self.layer?.anchorPoint = CGPoint(x: 1, y: 1)
self.frame = oldFrame
let animate = CABasicAnimation(keyPath: "transform.rotation")
animate.duration = duration
animate.repeatCount = Float.infinity
animate.fromValue = 0.0
animate.toValue = Double.pi * 2.0
self.layer?.add(animate, forKey: kAnimationKey)
oldFrame = self.frame
self.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.frame = oldFrame
}
}
}