I am trying to get UIView object from CAAnimation. I have implemented the following CAAnimationDelegate method
public func animationDidStop(_ animation:CAAnimation, finished:Bool) {
// Need respective View from "animation:CAAnimation"
}
This class will be performing multiple animations with different views. So I need to find out which View's animation is completed in this delegate method. Please guide me if there is any possibility to get the view from this animation.
As matt suggested here is the way you can find which animation has been completed.
First of all you need to add different key value to your animation when you are creating it like shown below:
let theAnimation = CABasicAnimation(keyPath: "opacity")
theAnimation.setValue("animation1", forKey: "id")
theAnimation.delegate = self
let theAnimation2 = CABasicAnimation(keyPath: "opacity")
theAnimation2.setValue("animation2", forKey: "id")
theAnimation2.delegate = self
And in animationDidStop method you can identify animations:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let val = anim.value(forKey: "id") as? String {
switch val {
case "animation1":
print("animation1")
case "animation2":
print("animation2")
default:
break
}
}
}
I have taken THIS answer and converted Objective c code to swift with switch case.
I just use tag property in UIView
animatedView.tag = 10
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
for myView in view.subviews {
if myView.tag == 10 {
myView.removeFromSuperview()
return
}
}
}
Related
:)
I want make some change to source code for 17-custom-presentation-controller to make custom transition animation,all my change is in class PopAnimator.
all my code is in here : https://github.com/hopy11/TransitionTest
This is my changes:
add a new instance variable in PopAnimator to save transitionContext:
var ctx:UIViewControllerContextTransitioning!
I rewrite the method:
animateTransition(using transitionContext: UIViewControllerContextTransitioning)
to this :
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
//save transitionContext
ctx = transitionContext
containerView.addSubview(toView)
let animation = CATransition()
animation.duration = duration / 2
animation.type = "cube"
//use type kCATransitionReveal is not work too ...
//animation.type = kCATransitionReveal
animation.subtype = kCATransitionFromLeft
animation.delegate = self
containerView.layer.add(animation, forKey: nil)
}
3.Last I make class PopAnimator confirm CAAnimationDelegate delegate, and add the new method:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if ctx != nil{
//make transition complete
ctx.completeTransition(true)
dismissCompletion?()
}
}
When run the App,the transition animation is look good when detailsVC dismiss,but nothing happened when presenting detailsVC!
You can see the demo above : when user tap the bottom left green view,it present detailsVC,but no animation happened!!! but when dismiss detailsVC,the animation work fine!
What’s wrong with it???
and how to fix it???
all my code is in here : https://github.com/hopy11/TransitionTest
thanks a lot! :)
iOS 10 added a new function for custom animated view controller transitions called
interruptibleAnimator(using:)
Lots of people appear to be using the new function, however by simply implementing their old animateTransition(using:) within the animation block of a UIViewPropertyAnimator in interruptibleAnimator(using:) (see Session 216 from 2016)
However I can't find a single example of someone actually using the interruptible animator for creating interruptible transitions. Everyone seems to support it, but no one actually uses it.
For example, I created a custom transition between two UIViewControllers using a UIPanGestureRecognizer. Both view controllers have a backgroundColor set, and a UIButton in the middle that changes the backgroundColour on touchUpInside.
Now I've implemented the animation simply as:
Setup the toViewController.view to be positioned to the
left/right (depending on the direction needed) of the
fromViewController.view
In the UIViewPropertyAnimator animation block, I slide the
toViewController.view into view, and the fromViewController.view out
of view (off screen).
Now, during transition, I want to be able to press that UIButton. However, the button press was not called. Odd, this is how the session implied things should work, I setup a custom UIView to be the view of both of my UIViewControllers as follows:
class HitTestView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view is UIButton {
print("hit button, point: \(point)")
}
return view
}
}
class ViewController: UIViewController {
let button = UIButton(type: .custom)
override func loadView() {
self.view = HitTestView(frame: UIScreen.main.bounds)
}
<...>
}
and logged out the func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? results. The UIButton is being hitTested, however, the buttons action is not called.
Has anyone gotten this working?
Am I thinking about this wrong and are interruptible transitions just to pausing/resuming a transition animation, and not for interaction?
Almost all of iOS11 uses what I believe are interruptible transitions, allowing you to, for example, pull up control centre 50% of the way and interact with it without releasing the control centre pane then sliding it back down. This is exactly what I wish to do.
Thanks in advance! Spent way to long this summer trying to get this working, or finding someone else trying to do the same.
I have published sample code and a reusable framework that demonstrates interruptible view controller animation transitions. It's called PullTransition and it makes it easy to either dismiss or pop a view controller simply by swiping downward. Please let me know if the documentation needs improvement. I hope this helps!
Here you go! A short example of an interruptible transition. Add your own animations in the addAnimation block to get things going.
class ViewController: UIViewController {
var dismissAnimation: DismissalObject?
override func viewDidLoad() {
super.viewDidLoad()
self.modalPresentationStyle = .custom
self.transitioningDelegate = self
dismissAnimation = DismissalObject(viewController: self)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return dismissAnimation
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? DismissalObject else { return nil }
return animator
}
}
class DismissalObject: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning {
fileprivate var shouldCompleteTransition = false
var panGestureRecongnizer: UIPanGestureRecognizer!
weak var viewController: UIViewController!
fileprivate var propertyAnimator: UIViewPropertyAnimator?
var startProgress: CGFloat = 0.0
var initiallyInteractive = false
var wantsInteractiveStart: Bool {
return initiallyInteractive
}
init(viewController: UIViewController) {
self.viewController = viewController
super.init()
panGestureRecongnizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
viewController.view.addGestureRecognizer(panGestureRecongnizer)
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 8.0 // slow animation for debugging
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {}
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
let animator = interruptibleAnimator(using: transitionContext)
if transitionContext.isInteractive {
animator.pauseAnimation()
} else {
animator.startAnimation()
}
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
// as per documentation, we need to return existing animator
// for ongoing transition
if let propertyAnimator = propertyAnimator {
return propertyAnimator
}
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else { fatalError("fromVC or toVC not found") }
let containerView = transitionContext.containerView
// Do prep work for animations
let duration = transitionDuration(using: transitionContext)
let timingParameters = UICubicTimingParameters(animationCurve: .easeOut)
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: timingParameters)
animator.addAnimations {
// animations
}
animator.addCompletion { [weak self] (position) in
let didComplete = position == .end
if !didComplete {
// transition was cancelled
}
transitionContext.completeTransition(didComplete)
self?.startProgress = 0
self?.propertyAnimator = nil
self?.initiallyInteractive = false
}
self.propertyAnimator = animator
return animator
}
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
initiallyInteractive = true
if !viewController.isBeingDismissed {
viewController.dismiss(animated: true, completion: nil)
} else {
propertyAnimator?.pauseAnimation()
propertyAnimator?.isReversed = false
startProgress = propertyAnimator?.fractionComplete ?? 0.0
}
break
case .changed:
let translation = gestureRecognizer.translation(in: nil)
var progress: CGFloat = translation.y / UIScreen.main.bounds.height
progress = CGFloat(fminf(fmaxf(Float(progress), -1.0), 1.0))
let velocity = gestureRecognizer.velocity(in: nil)
shouldCompleteTransition = progress > 0.3 || velocity.y > 450
propertyAnimator?.fractionComplete = progress + startProgress
break
case .ended:
if shouldCompleteTransition {
propertyAnimator?.startAnimation()
} else {
propertyAnimator?.isReversed = true
propertyAnimator?.startAnimation()
}
break
case .cancelled:
propertyAnimator?.isReversed = true
propertyAnimator?.startAnimation()
break
default:
break
}
}
}
I want to implement CABasicAnimation and have the UIViewController notified when the animation is completed. From this resource:
http://www.informit.com/articles/article.aspx?p=1168314&seqNum=2
I understood that I can specify the viewcontroller as a delegate for the animation and override animationDidStopmethod within the viewcontroller. However when I convert the following line of code into Swift:
[animation setDelegate:self];
like so:
animation.delegate = self //there are no setDelegate method
XCode complains:
Cannot assign value of type 'SplashScreenViewController' to type 'CAAnimationDelegate?'
What am I doing wrong? Am I missing something?
You need to make sure your viewController conforms to the CAAnimationDelegate.
class SplashScreenViewController: UIViewController, CAAnimationDelegate {
// your code, viewDidLoad and what not
override func viewDidLoad() {
super.viewDidLoad()
let animation = CABasicAnimation()
animation.delegate = self
// setup your animation
}
// MARK: - CAAnimation Delegate Methods
func animationDidStart(_ anim: CAAnimation) {
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
}
// Add any other CAAnimationDelegate Methods you want
}
You can also conform to the delegate by use of an extension:
extension SplasScreenViewController: CAAnimationDelegate {
func animationDidStart(_ anim: CAAnimation) {
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
}
}
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)
}
What I am trying to do is a custom animation of pushing ViewController from the left side.
I have created my custom transitioning delegate and I provide my custom animation, and everything works fine (new view slides from the left side).
The only problem is that push animation in iOS isn't only about sliding a view from the right side. The VC being obscured is also slightly moving in the same directions as the VC being pushed. Also, navigation bar kinda blinks. I can of course try to imitate this behaviour by guessing what the parameters should be (for example how much the VC being obscured moves on different iPhones), but maybe it is possible to find the values somewhere?
Help greatly appreciated.
I would create a UIViewControllerAnimatedTransitioning protocol abiding object
class CustomHorizontalSlideTransition: NSObject, UIViewControllerAnimatedTransitioning {
var operation: UINavigationControllerOperation = .Push
convenience init(operation: UINavigationControllerOperation) {
self.init()
self.operation = operation
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let disappearingVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let appearingVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let bounds = UIScreen.mainScreen().bounds
if self.operation == .Push {
appearingVC.view.frame = CGRectOffset(bounds, -bounds.size.height, 0)
containerView!.addSubview(disappearingVC.view)
containerView!.addSubview(appearingVC.view)
} else {
appearingVC.view.frame = bounds
disappearingVC.view.frame = bounds
containerView!.addSubview(appearingVC.view)
containerView!.addSubview(disappearingVC.view)
}
UIView.animateWithDuration(transitionDuration(transitionContext),
delay: 0.0,
options: UIViewAnimationOptions.CurveEaseInOut,
animations: { () -> Void in
if self.operation == .Push {
appearingVC.view.frame = bounds
} else {
disappearingVC.view.frame = CGRectOffset(bounds, -bounds.size.width, 0)
}
}) { (complete) -> Void in
transitionContext.completeTransition(true)
}
}
}
Then in your "From" and "To" view controllers, set the navigationController's delegate to self in view ViewDidAppear
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
navigationController?.delegate = self
}
The in both view controllers, override the following to provide a transitionAnimatedTransition delegate method and return the protocol abiding instance for your animation
override func transitionAnimatedTransition(operation: UINavigationControllerOperation) -> UIViewControllerAnimatedTransitioning? {
return CustomHorizontalSlideTransition(operation: operation)
}