Shrink ViewController to circular shape transition - swift - ios

I'm trying to follow the example in this project to create an expand/shrink view controller transition. In my case, the view controller which is being presented will have the same background color as the button itself, therefore this animation is essentially just the button expanding to cover the whole screen without ever disappearing.
https://github.com/AladinWay/TransitionButton
I've placed my button on the bottom right of the screen and managed to get it to expand using the following function:
private func expand(completion:(()->Void)?, revertDelay: TimeInterval) {
let expandAnim = CABasicAnimation(keyPath: "transform.scale")
let expandScale = (UIScreen.main.bounds.size.height/self.frame.size.height)*2
expandAnim.fromValue = 1.0
expandAnim.toValue = max(expandScale,26.0)
expandAnim.timingFunction = expandCurve
expandAnim.duration = 0.3
expandAnim.fillMode = .forwards
expandAnim.isRemovedOnCompletion = false
CATransaction.setCompletionBlock {
completion?()
// We return to original state after a delay to give opportunity to custom transition
DispatchQueue.main.asyncAfter(deadline: .now() + revertDelay) {
self.setOriginalState(completion: nil)
self.layer.removeAllAnimations() // make sure we remove all animation
}
}
layer.add(expandAnim, forKey: expandAnim.keyPath)
CATransaction.commit()
}
Now I'm trying to create the animation for dismissing the view controller which would essentially reverse the initial animation and shrink the whole screen back to the circular button on the bottom of the screen. I'm not sure how to adapt the above code to reverse this though.

You could try using the exact same block, except switch the fromValue and toValue.
expandAnim.fromValue = max(expandScale,26.0)
expandAnim.toValue = 1.0

Related

How to synchronise UIViewControllerInteractiveTransitioning and CABasicAnimation

I have a button in a view controller that has a shadow. This shadow is applied with an animation, in viewWillAppear.
The button is inside an empty view called 'buttonContainer'.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
CATransaction.begin()
CATransaction.setCompletionBlock {
self.buttonContainer.layer.shadowOpacity = 1
}
let animation = CABasicAnimation(keyPath: "shadowOpacity")
animation.fromValue = buttonContainer.layer.shadowOpacity
animation.toValue = 1
animation.duration = 0.6
buttonContainer.layer.add(animation, forKey: animation.keyPath)
CATransaction.commit()
}
I want the shadow to animate with the push animation of the view controller. I started implementing 'UIViewControllerAnimatedTransitioning' and I couldn't find a way to tie in the animation duration with CABasicAnimation.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let button = containerView.viewWithTag(68) as! UIButton
let animation = CABasicAnimation(keyPath: "shadowOpacity")
animation.fromValue = ?
animation.toValue = ?
}
And I suppose I would need to implement UIViewControllerInteractiveTransitioning but I can't think of a way to tie in CABasicAnimation. Any help appreciated
After doing some reading around it looks like someone has implemented a bridge between lower level CALayer animations and 'UIViewControllerInteractiveTransitioning'
https://github.com/stringcode86/UIPercentDrivenInteractiveTransitionWithCABasicAnimation/blob/master/InteractiveTransition/SCPercentDrivenInteractiveTransition.m
But it was done a long time ago and I haven't had time to verify it. One thing for sure is that it looks pretty ugly.
The UIViewPropertyAnimator class was released in iOS 10 and was meant to help out with this bridging by providing coordination with functions like 'addAnimation' and 'addCompletion'.
These guys have managed to implement a clean solution for bridging animations of 'CALayer' with 'UIViewControllerInteractiveTransitioning' via UIViewPropertyAnimator.
https://github.com/hedjirog/CustomPresentation
In summary
It's too hard. Don't bother.

UIView animation snaps back to original state before transitioning to a new view

I have some UIButtons that I'm animating indefinitely. The buttons all have 3 sublayers that are added, each of which have their own animation. I'm initializing these animations on viewDidAppear which works great - they fade in and start rotating. The problem is that when I transition to a new view, the animations seem to "snap" back to their initial state, then back to some other state right before the transition occurs. I've tried explicitly removing all of the animations on viewWillDisappear, even tried hiding the entire UIButton itself, but nothing seems to prevent this weird snapping behavior from occurring.
Here's a gif of what's happening (this is me transitioning back and forth between two views):
func animateRotation() {
// Gets called on viewDidAppear
let rotationRight: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationRight.toValue = Double.pi * 2
rotationRight.duration = 4
rotationRight.isCumulative = true
rotationRight.repeatCount = Float.greatestFiniteMagnitude
rotationRight.isRemovedOnCompletion = false
rotationRight.fillMode = .forwards
let rotationLeft: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationLeft.toValue = Double.pi * -2
rotationLeft.duration = 3
rotationLeft.isCumulative = true
rotationLeft.repeatCount = Float.greatestFiniteMagnitude
rotationLeft.isRemovedOnCompletion = false
rotationLeft.fillMode = .forwards
let circleImage1 = UIImage(named: "circle_1")?.cgImage
circleLayer1.frame = self.bounds
circleLayer1.contents = circleImage1
circleLayer1.add(rotationRight, forKey: "rotationAnimation")
layer.addSublayer(circleLayer1)
let circleImage2 = UIImage(named: "circle_2")?.cgImage
circleLayer2.frame = self.bounds
circleLayer2.contents = circleImage2
circleLayer2.add(rotationLeft, forKey: "rotationAnimation")
layer.addSublayer(circleLayer2)
let circleImage3 = UIImage(named: "circle_3")?.cgImage
circleLayer3.frame = self.bounds
circleLayer3.contents = circleImage3
circleLayer3.add(rotationRight, forKey: "rotationAnimation")
layer.addSublayer(circleLayer3)
}
I would think something as simple as this would hide the button completely as soon as it knows it's going away:
override func viewWillDisappear(_ animated: Bool) {
animatedButton.isHidden = true
}
What's also interesting is that this seems to stop happening if I let it run for a couple minutes. This tells me that it might be some sort of race condition. Just not sure what that race might be...
When you use the UIView set of animations they set the actual state to the final state and then start the animation, ie:
UIView.animate(withDuration: 1) { view.alpha = 0 }
If you check alpha right after the animation starts, it's already 0.
This is a convenience that UIView does for you but that CALayer does not. When you set a CALayer animation you are only setting the animation value, not the actual value of the variable, so when the animation is done, the layer snaps back to its original value. Check the layer value while you are in the middle of the animation and you will see the real value has not changed; only the animated value in the presentation layer has changed.
If you want to replicate the UIView behavior you need to either set the actual final value not he layer when the animation begins or in the use the delegate to set it when the animation ends.

Preserving stokeEnd change from animation without isRemovedOnCompletion set to false

I have a progress circle that animates. I can hit a button and it animates to 20%, if I hit the button again it animates to 40%. I am setting isRemovedOnCompletion to false. However each time I perform an animation a new animation is added to the CAShapelayer. I imagine this is not good for performance. Is there a better way to do this?
Dummy code:
#IBAction func didTapAnimate(_ sender: Any) {
let animateStroke = CABasicAnimation(keyPath: "strokeEnd")
animateStroke.fromValue = index > 0 ? progressPts[index - 1] : 0
animateStroke.toValue = progressPts[index]
animateStroke.duration = 2.0
animateStroke.fillMode = .forwards
animateStroke.isRemovedOnCompletion = false
circleLayer.add(animateStroke, forKey: "MyAnimation")
index+=1
}
I think no need to remove previous animation as per my knowledge only one animation layer is used for single key so animation automatically replace with previous one.
A string that identifies the animation. Only one animation per unique
key is added to the layer. The special key kCATransition is
automatically used for transition animations. You may specify nil for
this parameter.
So only the last animation occupies the memory previous animation automatically deallocated.
Just remove previous animation from circleLayer by calling removeAnimation(forKey:"yourkey")
#IBAction func didTapAnimate(_ sender: Any) {
circleLayer.removeAnimation(forKey: "MyAnimation")
let animateStroke = CABasicAnimation(keyPath: "strokeEnd")
animateStroke.fromValue = index > 0 ? progressPts[index - 1] : 0
animateStroke.toValue = progressPts[index]
animateStroke.duration = 2.0
animateStroke.fillMode = .forwards
animateStroke.isRemovedOnCompletion = false
circleLayer.add(animateStroke, forKey: "MyAnimation")
index+=1
}

View Controller Transition animate subview position

I'm trying to create a simple transition animation between two view controllers, both of which have the same label. I simply want to animate the label from its position in the first view controller, to its position in the second (see below illustration).
I have set up my view controllers to use a custom animation controller, where I have access to both view controllers and the label through an outlet.
In the animation block, I simply set the frame of the label on the first view controller to that of the label on the second view controller.
[UIView animateWithDuration:self.duration animations:^{
fromViewController.label.frame = toViewController.titleLabel.frame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
Instead of the intended effect of the label moving from the middle of the screen to the upper left corner, as soon as the animation begins the label is positioned in the bottom right corner and then animates to the middle.
I tried printing out the positions of the labels beforehand, which shows the same frame I see in the storyboard:
fromViewController.label.frame: {{115.5, 313}, {144, 41}}
toViewController.titleLabel.frame: {{16, 12}, {144, 41}}
I have no idea as to why I'm not getting the intended behavior, and what is happening in its place.
Any suggestions as to what I can change to make my animation run correctly and why I'm seeing this behavior would be greatly appreciated.
You mention the animation of the subviews but you don't talk about the overall animation, but I'd be inclined to use the container view for the animation, to avoid any potential confusion/problems if you're animating the subview and the main view simultaneously. But I'd be inclined to:
Make snapshots of where the subviews in the "from" view and then hide the subviews;
Make snapshots of where the subviews in the "to" view and then hide the subviews;
Convert all of these frame values to the coordinate space of the container and add all of these snapshots to the container view;
Start the "to" snapshots' alpha at zero (so they fade in);
Animate the changing of the "to" snapshots to their final destination changing their alpha back to 1.
Simultaneously animate the "from" snapshots to the location of the "to" view final destination and animate their alpha to zero (so they fade out, which combined with point 4, yields a sort of cross dissolve).
When all done, remove the snapshots and unhide the subviews whose snapshots were animated.
The net effect is a sliding of the label from one location to another, and if the initial and final content were different, yielding a cross dissolve while they're getting moved.
For example:
By using the container view for the animation of the snapshots, it's independent of any animation you might be doing of the main view of the destination scene. In this case I'm sliding it in from the right, but you can do whatever you want.
Or, you can do this with multiple subviews:
(Personally, if this were the case, where practically everything was sliding around, I'd lose the sliding animation of the main view because it's now becoming distracting, but it gives you the basic idea. Also, in my dismiss animation, I swapped around which view is being to another, which you'd never do, but I just wanted to illustrate the flexibility and the fading.)
To render the above, I used the following in Swift 4:
protocol CustomTransitionOriginator {
var fromAnimatedSubviews: [UIView] { get }
}
protocol CustomTransitionDestination {
var toAnimatedSubviews: [UIView] { get }
}
class Animator: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case present
case dismiss
}
let type: TransitionType
init(type: TransitionType) {
self.type = type
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewController(forKey: .from) as! CustomTransitionOriginator & UIViewController
let toVC = transitionContext.viewController(forKey: .to) as! CustomTransitionDestination & UIViewController
let container = transitionContext.containerView
// add the "to" view to the hierarchy
toVC.view.frame = fromVC.view.frame
if type == .present {
container.addSubview(toVC.view)
} else {
container.insertSubview(toVC.view, belowSubview: fromVC.view)
}
toVC.view.layoutIfNeeded()
// create snapshots of label being animated
let fromSnapshots = fromVC.fromAnimatedSubviews.map { subview -> UIView in
// create snapshot
let snapshot = subview.snapshotView(afterScreenUpdates: false)!
// we're putting it in container, so convert original frame into container's coordinate space
snapshot.frame = container.convert(subview.frame, from: subview.superview)
return snapshot
}
let toSnapshots = toVC.toAnimatedSubviews.map { subview -> UIView in
// create snapshot
let snapshot = subview.snapshotView(afterScreenUpdates: true)!// UIImageView(image: subview.snapshot())
// we're putting it in container, so convert original frame into container's coordinate space
snapshot.frame = container.convert(subview.frame, from: subview.superview)
return snapshot
}
// save the "to" and "from" frames
let frames = zip(fromSnapshots, toSnapshots).map { ($0.frame, $1.frame) }
// move the "to" snapshots to where where the "from" views were, but hide them for now
zip(toSnapshots, frames).forEach { snapshot, frame in
snapshot.frame = frame.0
snapshot.alpha = 0
container.addSubview(snapshot)
}
// add "from" snapshots, too, but hide the subviews that we just snapshotted
// associated labels so we only see animated snapshots; we'll unhide these
// original views when the animation is done.
fromSnapshots.forEach { container.addSubview($0) }
fromVC.fromAnimatedSubviews.forEach { $0.alpha = 0 }
toVC.toAnimatedSubviews.forEach { $0.alpha = 0 }
// I'm going to push the the main view from the right and dim the "from" view a bit,
// but you'll obviously do whatever you want for the main view, if anything
if type == .present {
toVC.view.transform = .init(translationX: toVC.view.frame.width, y: 0)
} else {
toVC.view.alpha = 0.5
}
// do the animation
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
// animate the snapshots of the label
zip(toSnapshots, frames).forEach { snapshot, frame in
snapshot.frame = frame.1
snapshot.alpha = 1
}
zip(fromSnapshots, frames).forEach { snapshot, frame in
snapshot.frame = frame.1
snapshot.alpha = 0
}
// I'm now animating the "to" view into place, but you'd do whatever you want here
if self.type == .present {
toVC.view.transform = .identity
fromVC.view.alpha = 0.5
} else {
fromVC.view.transform = .init(translationX: fromVC.view.frame.width, y: 0)
toVC.view.alpha = 1
}
}, completion: { _ in
// get rid of snapshots and re-show the original labels
fromSnapshots.forEach { $0.removeFromSuperview() }
toSnapshots.forEach { $0.removeFromSuperview() }
fromVC.fromAnimatedSubviews.forEach { $0.alpha = 1 }
toVC.toAnimatedSubviews.forEach { $0.alpha = 1 }
// clean up "to" and "from" views as necessary, in my case, just restore "from" view's alpha
fromVC.view.alpha = 1
fromVC.view.transform = .identity
// complete the transition
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
// My `UIViewControllerTransitioningDelegate` will specify this presentation
// controller, which will clean out the "from" view from the hierarchy when
// the animation is done.
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}
Then, to allow all of the above to work, if I'm transitioning from ViewController to SecondViewController, I'd specify what subviews I'm moving from and which ones I'm moving to:
extension ViewController: CustomTransitionOriginator {
var fromAnimatedSubviews: [UIView] { return [label] }
}
extension SecondViewController: CustomTransitionDestination {
var toAnimatedSubviews: [UIView] { return [label] }
}
And to support the dismiss, I'd add the converse protocol conformance:
extension ViewController: CustomTransitionDestination {
var toAnimatedSubviews: [UIView] { return [label] }
}
extension SecondViewController: CustomTransitionOriginator {
var fromAnimatedSubviews: [UIView] { return [label] }
}
Now, I don't want you to get lost in all of this code, so I'd suggest focusing on the high-level design (those first seven points I enumerated at the top). But hopefully this is enough for you to follow the basic idea.
The problem lies in dealing with coordinate systems. Consider these numbers:
fromViewController.label.frame: {{115.5, 313}, {144, 41}}
toViewController.titleLabel.frame: {{16, 12}, {144, 41}}
Those pairs of numbers are unrelated:
The frame of the label is in the bounds coordinates of its superview, probably fromViewController.view.
The frame of the titleLabel is in the bounds coordinates of its superview, probably toViewController.view.
Moreover, in most custom view transitions, the two view controller's views are in motion throughout the process. This makes it very difficult to say where the intermediate view should be at any moment in terms of either of them.
Thus you need to express the motion of this view in some common coordinate system, higher than either of those. That's why, in my answer here I use a snapshot view that's loose in the higher context view.

How to animate tabbar badge count in swift

I want to animate badge count over tabbar something like Bouncing Animation . Has anyone implemented it with native UITabBarController . i am not using any third party class for adding UITabBarController in my project.
i do some thing like this before i will share the code with you
first i create two functions
first one is :
func loopThrowViews(view:UIView){
for subview in (view.subviews){
let type = String(describing: type(of: subview))
print(type)
if type == "_UIBadgeView" {
print("this is BadgeView")
animateView(view: subview)
}
else {
loopThrowViews(view:subview)
}
}
}
this function take view and loop throw all its subViews until it find the badge View then it's call the animate method this one
func animateView(view:UIView){
let shakeAnimation = CABasicAnimation(keyPath: "position")
shakeAnimation.duration = 0.05
shakeAnimation.repeatCount = 50
shakeAnimation.autoreverses = true
shakeAnimation.fromValue = NSValue(cgPoint: CGPoint(x:view.center.x - 10, y:view.center.y))
shakeAnimation.toValue = NSValue(cgPoint: CGPoint(x:view.center.x + 10, y:view.center.y))
view.layer.add(shakeAnimation, forKey: "position")
}
You can replace the code at this method with your own animation
all what you need is to call this method like this when ever you want to animate the badge
loopThrowViews(view: self.tabBarController!.tabBar)
the result will be like this
full example here https://github.com/AliAdam/AnimateTabbarBadgeView

Resources