Related
I'm trying to implement CardViews similar to the ones used in the iOS 11 App Store. In order to do so, I'm using a GitHub project (https://github.com/PaoloCuscela/Cards) and tweaked it a bit.
The problem is that when transitioning back from the presented Detail View to the initial view (which is placed inside a TabBarController) the card is drawn in front of the TabBar (see video https://youtu.be/qDb3JoISTdw) which gives the whole transition a kind of 'glitchy' look.
This is the Code of the transitioning class I use:
import UIKit
class Animator: NSObject, UIViewControllerAnimatedTransitioning {
fileprivate var presenting: Bool
fileprivate var velocity = 0.6
var bounceIntensity: CGFloat = 0.07
var card: Card
init(presenting: Bool, from card: Card) {
self.presenting = presenting
self.card = card
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Animation Context Setup
let container = transitionContext.containerView
let to = transitionContext.viewController(forKey: .to)!
let from = transitionContext.viewController(forKey: .from)!
container.addSubview(to.view)
container.addSubview(from.view)
guard presenting else {
// Detail View Controller Dismiss Animations
card.isPresenting = false
let detailVC = from as! DetailViewController
let cardBackgroundFrame = detailVC.scrollView.convert(card.backgroundIV.frame, to: nil)
let bounce = self.bounceTransform(cardBackgroundFrame, to: card.originalFrame)
// Blur and fade with completion
UIView.animate(withDuration: velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.blurView.alpha = 0
}, completion: nil)
UIView.animate(withDuration: velocity, delay: 0, options: .curveEaseOut, animations: {
detailVC.snap.alpha = 0
self.card.backgroundIV.layer.cornerRadius = self.card.cardRadius
}, completion: { _ in
detailVC.layout(self.card.originalFrame, isPresenting: false, isAnimating: false)
self.card.addSubview(detailVC.card.backgroundIV)
transitionContext.completeTransition(true)
})
// Layout with bounce effect
UIView.animate(withDuration: velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.layout(self.card.originalFrame, isPresenting: false, transform: bounce)
self.card.delegate?.cardIsHidingDetail?(card: self.card)
}) { _ in UIView.animate(withDuration: self.velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.layout(self.card.originalFrame, isPresenting: false)
self.card.delegate?.cardIsHidingDetail?(card: self.card)
})
}
return
}
// Detail View Controller Present Animations
card.isPresenting = true
let detailVC = to as! DetailViewController
let bounce = self.bounceTransform(card.originalFrame, to: card.backgroundIV.frame)
container.bringSubview(toFront: detailVC.view)
detailVC.card = card
detailVC.layout(card.originalFrame, isPresenting: false)
// Blur and fade with completion
UIView.animate(withDuration: velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.blurView.alpha = 1
}, completion: nil)
UIView.animate(withDuration: velocity, delay: 0, options: .curveEaseOut, animations: {
self.card.transform = CGAffineTransform.identity // Reset card identity after push back on tap
detailVC.snap.alpha = 1
self.card.backgroundIV.layer.cornerRadius = 0
}, completion: { _ in
detailVC.layout(self.card.originalFrame, isPresenting: true, isAnimating: false, transform: .identity)
transitionContext.completeTransition(true)
})
// Layout with bounce effect
UIView.animate(withDuration: velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.layout(detailVC.view.frame, isPresenting: true, transform: bounce)
self.card.delegate?.cardIsShowingDetail?(card: self.card)
}) { _ in UIView.animate(withDuration: self.velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.layout(detailVC.view.frame, isPresenting: true)
self.card.delegate?.cardIsShowingDetail?(card: self.card)
})
}
}
private func bounceTransform(_ from: CGRect, to: CGRect ) -> CGAffineTransform {
let old = from.center
let new = to.center
let xDistance = old.x - new.x
let yDistance = old.y - new.y
let xMove = -( xDistance * bounceIntensity )
let yMove = -( yDistance * bounceIntensity )
return CGAffineTransform(translationX: xMove, y: yMove)
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return velocity
}
}
I haven't worked with transitioning in iOS and hope someone can tell me how to achieve what I want here.
UITabBarController does all of its layout using autoresizing masks. That being the case you can grab the tabBar add it to the container view, perform the animation then add it back to it's root view. For example using the Cards animation you can change the animateTransition(using transitionContext: UIViewControllerContextTransitioning) to:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Animation Context Setup
let container = transitionContext.containerView
let to = transitionContext.viewController(forKey: .to)!
let from = transitionContext.viewController(forKey: .from)!
container.addSubview(to.view)
container.addSubview(from.view)
// If going to tab bar controller
// Add tab bar above view controllers
// Turn off interactions
if !presenting, let tabController = to as? UITabBarController {
tabController.tabBar.isUserInteractionEnabled = false
container.addSubview(tabController.tabBar)
}
guard presenting else {
// Detail View Controller Dismiss Animations
card.isPresenting = false
let detailVC = from as! DetailViewController
let cardBackgroundFrame = detailVC.scrollView.convert(card.backgroundIV.frame, to: nil)
let bounce = self.bounceTransform(cardBackgroundFrame, to: card.originalFrame)
// Blur and fade with completion
UIView.animate(withDuration: velocity, delay: 0, options: .curveEaseOut, animations: {
detailVC.blurView.alpha = 0
detailVC.snap.alpha = 0
self.card.backgroundIV.layer.cornerRadius = self.card.cardRadius
}, completion: { _ in
detailVC.layout(self.card.originalFrame, isPresenting: false, isAnimating: false)
self.card.addSubview(detailVC.card.backgroundIV)
// Add tab bar back to tab bar controller's root view
if let tabController = to as? UITabBarController {
tabController.tabBar.isUserInteractionEnabled = true
tabController.view.addSubview(tabController.tabBar)
}
transitionContext.completeTransition(true)
})
// Layout with bounce effect
UIView.animate(withDuration: velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.layout(self.card.originalFrame, isPresenting: false, transform: bounce)
self.card.delegate?.cardIsHidingDetail?(card: self.card)
}) { _ in UIView.animate(withDuration: self.velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.layout(self.card.originalFrame, isPresenting: false)
self.card.delegate?.cardIsHidingDetail?(card: self.card)
})
}
return
}
// Detail View Controller Present Animations
card.isPresenting = true
let detailVC = to as! DetailViewController
let bounce = self.bounceTransform(card.originalFrame, to: card.backgroundIV.frame)
container.bringSubview(toFront: detailVC.view)
detailVC.card = card
detailVC.layout(card.originalFrame, isPresenting: false)
// Blur and fade with completion
UIView.animate(withDuration: velocity, delay: 0, options: .curveEaseOut, animations: {
self.card.transform = CGAffineTransform.identity // Reset card identity after push back on tap
detailVC.blurView.alpha = 1
detailVC.snap.alpha = 1
self.card.backgroundIV.layer.cornerRadius = 0
}, completion: { _ in
detailVC.layout(self.card.originalFrame, isPresenting: true, isAnimating: false, transform: .identity)
transitionContext.completeTransition(true)
})
// Layout with bounce effect
UIView.animate(withDuration: velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.layout(detailVC.view.frame, isPresenting: true, transform: bounce)
self.card.delegate?.cardIsShowingDetail?(card: self.card)
}) { _ in UIView.animate(withDuration: self.velocity/2, delay: 0, options: .curveEaseOut, animations: {
detailVC.layout(detailVC.view.frame, isPresenting: true)
self.card.delegate?.cardIsShowingDetail?(card: self.card)
})
}
}
Which produces an animation like:
I'm trying to add animation to my tabBarController when hidden. Im able to accomplish this effect with the navigationBarController by using self.navigationController?.isNavigationBarHidden = true. I'm able to hide the tabBar by using self.tabBarController?.tabBar.isHidden = true but i do not get the animation how can I do this thank you in advance.
You could change the tab bar's frame inside an animation, so something like:
func hideTabBar() {
var frame = self.tabBarController?.tabBar.frame
frame?.origin.y = self.view.frame.size.height + (frame?.size.height)!
UIView.animate(withDuration: 0.5, animations: {
self.tabBarController?.tabBar.frame = frame!
})
}
func showTabBar() {
var frame = self.tabBarController?.tabBar.frame
frame?.origin.y = self.view.frame.size.height - (frame?.size.height)!
UIView.animate(withDuration: 0.5, animations: {
self.tabBarController?.tabBar.frame = frame!
})
}
Which sets the tab bar just below the visible screen, so that it slides up/down from the bottom.
I've developed a util extension for UIViewController
Swift 4 compatible:
extension UIViewController {
func setTabBarHidden(_ hidden: Bool, animated: Bool = true, duration: TimeInterval = 0.3) {
if animated {
if let frame = self.tabBarController?.tabBar.frame {
let factor: CGFloat = hidden ? 1 : -1
let y = frame.origin.y + (frame.size.height * factor)
UIView.animate(withDuration: duration, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: y, width: frame.width, height: frame.height)
})
return
}
}
self.tabBarController?.tabBar.isHidden = hidden
}
}
Improvement of the response of #Luca Davanzo. If the bar is already hidden, it will continue hiding it and moving it lower. Also get rid of the return, so the state of the tabbar.hidden changes when the animation happens.
So I added a check:
extension UIViewController {
func setTabBarHidden(_ hidden: Bool, animated: Bool = true, duration: TimeInterval = 0.5) {
if self.tabBarController?.tabBar.isHidden != hidden{
if animated {
//Show the tabbar before the animation in case it has to appear
if (self.tabBarController?.tabBar.isHidden)!{
self.tabBarController?.tabBar.isHidden = hidden
}
if let frame = self.tabBarController?.tabBar.frame {
let factor: CGFloat = hidden ? 1 : -1
let y = frame.origin.y + (frame.size.height * factor)
UIView.animate(withDuration: duration, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: y, width: frame.width, height: frame.height)
}) { (bool) in
//hide the tabbar after the animation in case ti has to be hidden
if (!(self.tabBarController?.tabBar.isHidden)!){
self.tabBarController?.tabBar.isHidden = hidden
}
}
}
}
}
}
}
In case if you need to toggle it from hide to visible and vice versa:
func toggleTabbar() {
guard var frame = tabBarController?.tabBar.frame else { return }
let hidden = frame.origin.y == view.frame.size.height
frame.origin.y = hidden ? view.frame.size.height - frame.size.height : view.frame.size.height
UIView.animate(withDuration: 0.3) {
self.tabBarController?.tabBar.frame = frame
}
}
Swift 4 solution:
tabBarController?.tabBar.isHidden = true
UIView.transition(with: tabBarController!.view, duration: 0.35, options: .transitionCrossDissolve, animations: nil)
Here is a simple extension :
func setTabBar(hidden:Bool) {
guard let frame = self.tabBarController?.tabBar.frame else {return }
if hidden {
UIView.animate(withDuration: 0.3, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: frame.origin.y + frame.height, width: frame.width, height: frame.height)
})
}else {
UIView.animate(withDuration: 0.3, animations: {
self.tabBarController?.tabBar.frame = UITabBarController().tabBar.frame
})
}
}
So I've been playing around for 3 days with this now, finding out that the one that worked for me in my code was Adriana's post from 14th Sept 2018. But I was not sure how to use the coding once copied into my Project. So, after much experimenting I found that the way I could use this func was to put the following into into the respective swipe actions.
setTabBarHidden(false)
setTabBarHidden(true)
My next step is to try to get the swipe actions working while using UIScrollView in the same UIView at the same time.
You have to add UIView transitionWithView class func
Swift 2
func hideTabBarWithAnimation() -> () {
UIView.transitionWithView(tableView, duration: 1.0, options: .TransitionCrossDissolve, animations: { () -> Void in
self.tabBarController?.tabBar.hidden = true
}, completion: nil)
}
Swift 3, 4, 5
func hideTabBarWithAnimation() -> () {
UIView.transition(with: tableView, duration: 1.0, options: .transitionCrossDissolve, animations: { () -> Void in
self.tabBarController?.tabBar.isHidden = true
}, completion: nil)
}
I have a label that is initially positioned at the center of the screen. It currently transitions from the the center to the right end of the screen then autoreverses back to its position. I'd like to have it begin another animateWithDuration so that it continues from the center return to the left position of the screen then autoreverse back to the position and sequentially loop from there on after.
I have already attempted and successfully made the first half work but I'm not sure how to continue to the second portion where it begins the center->left transition and loop.
Swift 2.0 Code:
func animateRight()
{
UIView.animateWithDuration(1.0, delay: 0.0, options: [ .Autoreverse, .CurveEaseInOut], animations: {
label.center.x = self.view.frame.width/2
}, completion: { finished in
if finished {
label.frame.origin.x = 0.0
animateLeft()
}
})
}
func animateLeft()
{
UIView.animateWithDuration(1.0, delay: 0.0, options: [ .Autoreverse, .CurveEaseInOut], animations: {
label.frame.origin.x = (self.view.frame.width/2) * -1
}, completion: { finished in
if finished {
label.center.x = self.view.frame.width/2
animateRight()
}
})
}
// Start process
animateRight()
You should call the same animate with duration method you create for animating to right and call in completion. Something like this:
func animateRight()
{
UIView.animate(withDuration: 1.0, delay: 0.0, options: [], animations: {
self.label.center.x = self.view.frame.width
}, completion: { finished in
if finished {
self.animateLeft()
}
})
}
func animateLeft()
{
UIView.animate(withDuration: 2.0, delay: 0.0, options: [ .autoreverse, .repeat, .curveEaseInOut, .beginFromCurrentState], animations: {
self.label.frame.origin.x = 0.0
}, completion: nil)
}
Call defaultSetup to start
When animationRight end, it will call animationLeft
When animationLeft end, it will call animationRight again to run animation loop
Here is the code:
func defaultSetup(){
self.animationRight()
}
func animationRight(){
let transform = CGAffineTransform(scaleX: 1.05, y: 1.05).rotated(by: .pi/180)
UIView.animate(withDuration: 3.0, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3.0, options: [.allowUserInteraction, .repeat], animations: { [weak self] in
guard let `self` = self else { return }
self.vRight.transform = transform
}, completion: { [weak self] (done) in
guard let `self` = self else { return }
self.vRight.transform = .identity
self.animationLeft()
})
}
func animationLeft(){
let transform = CGAffineTransform(scaleX: 1.05, y: 1.05).rotated(by: .pi/180)
UIView.animate(withDuration: 3.0, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3.0, options: [.allowUserInteraction], animations: { [weak self] in
guard let `self` = self else { return }
self.vLeft.transform = transform
}, completion: { [weak self] (done) in
guard let `self` = self else { return }
self.vLeft.transform = .identity
self.animationRight()
})
}
I saw a tutorial on Appcoda Transition ViewControllers transition a menu from up to bottom and I implemented it. Then, I tried to transition from bottom up using UIViewControllerContextTransitioning. But, doing it wrong cause I was setting the wrong values I think. Below is the code
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
//Get reference to our fromView, toView and the container view
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
//Setup the transform for sliding
let container = transitionContext.containerView()
let height = container?.frame.height
let moveDown = CGAffineTransformMakeTranslation(0, height! - 150)
let moveUp = CGAffineTransformMakeTranslation(0, -50)
//Add both views to the container view
if isPresenting {
toView?.transform = moveUp
snapShot = fromView?.snapshotViewAfterScreenUpdates(true)
container?.addSubview(toView!)
container?.addSubview(snapShot!)
}
//Perform the animation
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.3, options: UIViewAnimationOptions(rawValue: 0), animations: {
if self.isPresenting {
self.snapShot?.transform = moveDown
toView?.transform = CGAffineTransformIdentity
} else {
self.snapShot?.transform = CGAffineTransformIdentity
fromView?.transform = moveUp
}
}, completion: {finished in
transitionContext.completeTransition(true)
if !self.isPresenting {
self.snapShot?.removeFromSuperview()
}
})
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return duration
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// Get reference to our fromView, toView and the container view
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// Set up the transform we'll use in the animation
guard let container = transitionContext.containerView() else {
return
}
let moveUp = CGAffineTransformMakeTranslation(0, container.frame.height + 50)
let moveDown = CGAffineTransformMakeTranslation(0, -250)
// Add both views to the container view
if isPresenting {
toView.transform = moveUp
snapshot = fromView.snapshotViewAfterScreenUpdates(true)
container.addSubview(toView)
container.addSubview(snapshot!)
}
// Perform the animation
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [], animations: {
if self.isPresenting {
self.snapshot?.transform = moveDown
toView.transform = CGAffineTransformIdentity
} else {
self.snapshot?.transform = CGAffineTransformIdentity
fromView.transform = moveUp
}
}, completion: { finished in
transitionContext.completeTransition(true)
if !self.isPresenting {
self.snapshot?.removeFromSuperview()
}
})
}
This Should work. I checked out the tutorial you shared and you probably can't see the menu on the bottom because the way the MenuTableViewController.swift is set up on storyboard it is made so that the menu is always started from the top, so change that up and it should work perfectly fine. Let me know if you have any questions.
I'm presenting a view from another using a UIViewControllerTransitioningDelegate instance as transitioning delegate and modalPresentationStyle = Custom.
I'm using a TableViewController using static table view cells with UITextfields inside. Now when tapping over a text field whose borders are close to the the keyboard's frame, part of the tableView beneath the keyboard shows up. I also removed a UITapGestureRecognizer that I added to the semi-transparent background to make sure it's not part of the problem but the issue it's still there. Any ideas? Below is the animateTransition() method
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
let containerView = transitionContext.containerView()
if let presentedViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) {
let presentedView = presentedViewController.view
let fromView = transitionContext.viewForKey(UITransitionContextFromViewControllerKey)
let centre = presentedView.center
if isPresenting {
presentedView.center = containerView.center
presentedView.frame = presentedView.bounds.rectByInsetting(dx: 30, dy: 150)
transitionContext.containerView().addSubview(presentedView)
dimmingView.frame = containerView.bounds
dimmingView.alpha = 0.0
containerView.insertSubview(dimmingView, atIndex: 0)
presentedViewController.transitionCoordinator()?.animateAlongsideTransition({
context in
self.dimmingView.alpha = 1.0
}, completion: nil)
}
else {
presentedViewController.transitionCoordinator()?.animateAlongsideTransition({
context in
self.dimmingView.alpha = 0.0
}, completion: {
context in
self.dimmingView.removeFromSuperview()
})
}
UIView.animateWithDuration(self.transitionDuration(transitionContext),
delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 10.0, options: nil,
animations: {
presentedView.center = centre
}, completion: {
_ in
transitionContext.completeTransition(true)
})
}
}