in my swift 2 app, i have this extension:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 2.5, completionDelegate: AnyObject? = nil) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2.0)
rotateAnimation.duration = duration
if let delegate: AnyObject = completionDelegate {
rotateAnimation.delegate = delegate
}
self.layer.addAnimation(rotateAnimation, forKey: nil)
}
}
with this code, i can rotate an image 360 degree.
now i would like to stop this animation directly after i pressed on a button.
in my view controller is an action for my button. if i press this button, the following value will set:
self.shouldStopRotating = true
and i have this code part in the same vc, too:
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if self.shouldStopRotating == false {
self.LoadingCircle.rotate360Degrees(completionDelegate: self)
}
}
the image will stop after i pressed the button, but it will stop after the animation will be finished (after 360 degrees) - but this is to late.
the image have to stop rotating directly on the actual position after i press the button
Try to add this when the button that stops the animation is presse:
self.LoadingCircle.layer.removeAllAnimations()
let currentLayer = self.LoadingCircle.layer.presentationLayer();
let currentRotation = currentLayer?.valueForKeyPath("transform.rotation.z")?.floatValue;
let rotation = CGAffineTransformMakeRotation(CGFloat(currentRotation!));
self.LoadingCircle.transform = rotation;
Related
So I have a tableviewController called SettingsViewController, and it has the following touchesEnded function:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
print("yoyoyoyoyoyoyoyQVEWEVIWNE")
let touchLocation = touch.location(in: view)
// 290 because the width of the view is 414, and the SettingsViewController width gets set to 0.7 * the view width in SlideInTransition. 0.7 * 414 is 289.8
if touchLocation.x > 200 {
dismiss(animated: true)
}
}
}
I made the print statement to see if it was being called, which it is not. This view controller is presented with a 'menu-esque' slide in custom transition. I have a suspicion that the bounds of the UIView is the problem somehow. Here's the custom transition code:
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting: Bool = false
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Make sure they exist
// The view controller being transitioned from, using the context (ex: here it's the MapViewController)
guard let fromViewController = transitionContext.viewController(forKey: .from),
// The view controller being transitioned to, using the context (ex: here it's the SettingsTableViewController)
let toViewController = transitionContext.viewController(forKey: .to) else {return}
let containerView = transitionContext.containerView
// Constants for appearance of SettingsViewController
let vcWidth = toViewController.view.bounds.width * 0.7
let vcHeight = toViewController.view.bounds.height
if isPresenting {
// Add SettingsViewController to container
containerView.addSubview(toViewController.view)
// Initial frame for view controller, off the screen to the left to start, that way it appears to slide in
toViewController.view.frame = CGRect(x: -vcWidth, y: 0, width: vcWidth, height: vcHeight)
}
// Animate view controller onto the screen, sliding in from left
let transform = {
toViewController.view.transform = CGAffineTransform(translationX: vcWidth, y: 0)
}
// Animate back off screen
let identity = {
// .identity returns the vc to the initial frame, as created above in the isPresenting if statement
fromViewController.view.transform = .identity
}
// Animation of the transition
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
// If presenting, transform SettingsViewController (to) onto screen, otherwise set it back off the screen.
self.isPresenting ? transform() : identity()
}) { (Bool) in
transitionContext.completeTransition(!isCancelled)
}
}
}
I made my touchesEnded code so that when the user touches outside the viewController, it dismisses, (the view controller only 70% the width of the screen) but it simply doesn't get called, regardless of where on the screen I tap. Any idea why? Thanks.
https://developer.apple.com/documentation/uikit/uiresponder/1621084-touchesended
«If you override this method without calling super (a common use pattern), you must also override the other methods for handling touch events, even if your implementations do nothing.»
This would be a start.
I use this code to create rotation animation for my imageView:
func rotate(imageView: UIImageView, aCircleTime: Double) {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = -Double.pi * 2
rotationAnimation.duration = aCircleTime
rotationAnimation.repeatCount = .infinity
imageView.layer.add(rotationAnimation, forKey: nil)
}
But how to pause this animation?
It's possible to pause and resume CAAnimations, but it's fussy and a little confusing. Take a look at this project on Github:
https://github.com/DuncanMC/ClockWipeSwift.git
It uses an extension on CALayer:
// Credit to Rand, from Stack Overflow, for the basis of this extension
// see https://stackoverflow.com/a/59079995/205185
import UIKit
import CoreGraphics
import Foundation
extension CALayer
{
func isPaused() -> Bool {
return speed == 0
}
private func internalPause(_ pause: Bool) {
if pause {
let pausedTime = convertTime(CACurrentMediaTime(), from: nil)
speed = 0.0
timeOffset = pausedTime
} else {
let pausedTime = timeOffset
speed = 1.0
timeOffset = 0.0
beginTime = 0.0
let timeSincePause = convertTime(CACurrentMediaTime(), from: nil) - pausedTime
beginTime = timeSincePause
}
}
func pauseAnimation(_ pause: Bool) {
if pause != isPaused() {
internalPause(pause)
}
}
func pauseOrResumeAnimation() {
internalPause(isPaused())
}
}
Pausing and resuming is much easier if you use UIView animation and UIViewPropertyAnimator. There are lots of tutorials that explain how to do it.
You can look at this project on Github that shows how to use UIViewPropertyAnimator.
What I Have
I am using UIViewControllerAnimatedTransitioning protocol with an attached UIViewPropertyAnimator to pan down to dismiss a View Controller
extension SecondViewController : UIViewControllerAnimatedTransitioning {
func interruptibleAnimator(using ctx: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if self.animator != nil {
return self.animator!
}
let containerView = ctx.containerView
let toVC = ctx.viewController(forKey: .to) as! FirstViewController
let fromVC = ctx.viewController(forKey: .from) as! SecondViewController
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
self.animator = UIViewPropertyAnimator(duration: transitionDuration(using: ctx),
curve: .easeOut, animations: {
self.fromVC.view.transform = CGAffineTransform(scale: 0.5)
})
self.animator.isInterruptible = true
self.animator.isUserInteractionEnabled = true
self.animator.isManualHitTestingEnabled = true
self.animator.addCompletion { position in
switch position {
case .end:
break
case .current:
break
case .start:
break
}
let cancelled = ctx.transitionWasCancelled
if (cancelled) {
//..
} else {
//..
}
ctx.completeTransition(!cancelled)
}
self.animator = anim
return self.animator
}
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
let animator = self.interruptibleAnimator(using: ctx)
self.animator.startAnimation()
}
func animationEnded(_ transitionCompleted: Bool) {
self.interactiveTransition = nil
self.animator = nil
}
}
Pan Gesture to handle the animation:
func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
let panTranslation = gestureRecognizer.translation(in: gestureRecognizer.view!)
var progress = panTranslation.y / (gestureRecognizer.view!.bounds.size.height * 0.5)
switch gestureRecognizer.state {
case .began:
self.interactiveTransition = UIPercentDrivenInteractiveTransition()
self.navigationController!.popViewController(animated: true)
case .changed:
self.interactiveTransition!.update(progress)
case .cancelled, .ended:
if progress > 0.5 {
//Complete Transition
let timingParameters = UICubicTimingParameters(animationCurve: .easeInOut)
self.animator!.continueAnimation!(withTimingParameters: timingParameters, durationFactor: progress)
self.animator?.addAnimations! {
//Completion Animations
}
self.interactiveTransition!.finish()
} else {
//Cancel Transition
self.animator!.isReversed = true
let timingParameters = UICubicTimingParameters(animationCurve: .easeInOut)
self.animator!.continueAnimation!(withTimingParameters: timingParameters, durationFactor: progress)
self.animator!.addAnimations!({
//Cancelling Animations
}, delayFactor: 0 )
self.interactiveTransition!.cancel()
}
default:
break
}
}
What Works
Swiping down to dismissal works perfectly. Swiping slightly down and lifting finger to cancel also works perfectly.
Issue
Swiping down and back up beyond starting point (where progress becomes negative) and lifting up the finger should cancel the transition with cancelling animation. This happens in iOS 10 but it first reverses the navigation controller transitions first, then snaps back. In iOS 11, cancelling animation happens, then I see navigation controller transition is reversed. If you wait, you can see navigation controller transition does try to correct it self in animation over 10 mins or so.
Issue with:
- self.interactiveTransition!.cancel()?
- self.interactiveTransition!.completionSpeed ??
I don't know if this is a bug or we're all just doing it wrong but to correct the behavior, add .completionSpeed = 0.999 to the interactionController in the .ended case of the pan gesture handler. It's a hack but at least it's only a single line.
I have a button, when it's tapped, it should rotate itself, here's my code:
#IBAction func calculateButtonTapped(_ sender: UIButton) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI)
rotateAnimation.speed = 3.0
rotateAnimation.repeatCount = 6000
calculateButton.layer.add(rotateAnimation, forKey: nil)
DispatchQueue.main.async {
self.openCircle(withCenter: sender.center, dataSource: self.calculator!.iterateWPItems())
self.calculateButton.layer.removeAllAnimations()
}
}
However, sometimes when I tap the button, it immediately goes back to normal state then rotates, sometimes the button changes to dark selected state, and doesn't animate at all, tasks after the animates will get finished. If I don't stop the animation, it starts after openCircle is finished.
What could be the cause?
You're not setting duration of your animation.
Replace this
rotateAnimation.speed = 3.0
with this
rotateAnimation.duration = 3.0
#alexburtnik and it's ok to block the main thread
No, it's not ok. You should add a completion parameter in openCircle method and call it whenever it's animation (or whatever) is finished. If you block main thread, you will have a frozen UI, which is strongly discouraged.
If you're unsure that calculateButtonTapped is called on main thread, you should dispatch first part of your method as well. Everything related to UI must be done on the main thread.
It should look similar to this:
#IBAction func calculateButtonTapped(_ sender: UIButton) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI)
rotateAnimation.duration = 3.0
rotateAnimation.repeatCount = .infinity //endless animation
calculateButton.layer.add(rotateAnimation, forKey: nil)
self.openCircle(
withCenter: sender.center,
dataSource: self.calculator!.iterateWPItems(),
completion: {
self.calculateButton.layer.removeAllAnimations()
})
}
func openCircle(withCenter: CGPoint, dataSource: DataSourceProtocol, completion: (()->Void)?) {
//do your staff and call completion when you're finished
//don't block main thread!
}
Try this out in order to rotate a button that is clicked by connecting the button to the action on a storyboard. You can of course call this function by passing any UIButton as the sender!
#IBAction func calculateButtonTapped(_ sender: UIButton) {
guard (sender.layer.animation(forKey: "rotate") == nil) else { return }
let rotationDuration: Float = 3.0
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.toValue = Float.pi * rotationDuration
animation.duration = CFTimeInterval(rotationDuration)
animation.repeatCount = .infinity
sender.layer.add(animation, forKey: "rotate")
}
Change the rotationDuration to whatever time length you want for a full rotation. You could also adjust the function further to take that as an argument.
Edit: Added a guard statement so that the rotations don't keep adding up every time that the button is tapped.
Thanks to everybody for answering, I found the solution myself after a crash course on multithreading, the problem is I blocked the main thread with openCircle method.
Here's the updated code:
#IBAction func calculateButtonTapped(_ sender: UIButton) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI)
rotateAnimation.speed = 3.0
rotateAnimation.repeatCount = .infinity
DispatchQueue.global(qos: .userInitiated).async {
self.openCircle(withCenter: sender.center, dataSource: self.calculator!.iterateWPItems()){}
DispatchQueue.main.sync {
self.calculateButton.layer.removeAllAnimations()
}
}
self.calculateButton.layer.add(rotateAnimation, forKey: nil)
}
I'm trying to make four UIButtons rotate in Swift. I got this:
import UIKit
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 1.0, completionDelegate: AnyObject? = nil) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2.0)
rotateAnimation.duration = duration
if let delegate: AnyObject = completionDelegate {
rotateAnimation.delegate = delegate
}
self.layer.addAnimation(rotateAnimation, forKey: nil)
}
}
But I need it to repeat. In my UIViewController I used the animationDidStop function but how can I know which of the four animation triggers it? It has a parameter called CAAnimation but I cannot compare it to anything. Any suggestion?
Re-write your method using CATransition and use the solution from this question:
How to identify CAAnimation within the animationDidStop delegate?