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 am creating a custom transition for my app but when I try to create a snapshot of the destination view it always appears as a blank white rectangle. It is important I note that this is a custom push transition and not a modal presentation of the view. When presenting modally the snapshot appears to work. Is this the normal behaviour for custom push/pop transitions?
The code I've written is as follows:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to) as? CultureViewController else {
return
}
let containerView = transitionContext.containerView
let finalFrame = transitionContext.finalFrame(for: toViewController)
let snapshot = toViewController.view.snapshotView(afterScreenUpdates: true)
snapshot?.frame = UIScreen.main.bounds
containerView.addSubview(snapshot!)
toViewController.view.transform = CGAffineTransform(translationX: 0, y: toViewController.view.bounds.height)
//containerView.addSubview(toViewController.view)
let duration = transitionDuration(using: transitionContext)
UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: {
toViewController.view.frame = finalFrame
})
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
Taking a snapshot of a view before it is rendered, such as is the case with a transition before you call containerView.addSubview(toViewController.view), returns a blank view, per UIView's documentation.
If the current view is not yet rendered, perhaps because it is not yet onscreen, the snapshot view has no visible content.
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)
})
}
}
My custom segue animation works fine, but sometimes I see white-flashes/flickering during the animation. Any tips or suggestions on how to avoid this?
This is my code :
import UIKit
#objc(InsetBlurModalSeque) class InsetBlurModalSeque: UIStoryboardSegue {
override func perform() {
let sourceViewController = self.sourceViewController as UIViewController
let destinationViewController = self.destinationViewController as UIViewController
// Make sure the background is ransparent
destinationViewController.view.backgroundColor = UIColor.clearColor()
// Take screenshot from source VC
UIGraphicsBeginImageContext(sourceViewController.view.bounds.size)
sourceViewController.view.drawViewHierarchyInRect(sourceViewController.view.frame, afterScreenUpdates:true)
var image:UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Blur screenshot
var blurredImage:UIImage = image.applyBlurWithRadius(5, tintColor: UIColor(white: 1.0, alpha: 0.0), saturationDeltaFactor: 1.3, maskImage: nil)
// Crop screenshot, add to view and send to back
let blurredBackgroundImageView : UIImageView = UIImageView(image:blurredImage)
blurredBackgroundImageView.clipsToBounds = true;
blurredBackgroundImageView.contentMode = UIViewContentMode.Center
let insets:UIEdgeInsets = UIEdgeInsetsMake(20, 20, 20, 20);
blurredBackgroundImageView.frame = UIEdgeInsetsInsetRect(blurredBackgroundImageView.frame, insets)
destinationViewController.view.addSubview(blurredBackgroundImageView)
destinationViewController.view.sendSubviewToBack(blurredBackgroundImageView)
// Add original screenshot behind blurred image
let backgroundImageView : UIImageView = UIImageView(image:image)
destinationViewController.view.addSubview(backgroundImageView)
destinationViewController.view.sendSubviewToBack(backgroundImageView)
// Add the destination view as a subview, temporarily – we need this do to the animation
sourceViewController.view.addSubview(destinationViewController.view)
// Set initial state of animation
destinationViewController.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 0.1, 0.1);
blurredBackgroundImageView.alpha = 0.0;
backgroundImageView.alpha = 0.0;
// Animate
UIView.animateWithDuration(0.5,
delay: 0.0,
usingSpringWithDamping: 0.6,
initialSpringVelocity: 0.0,
options: UIViewAnimationOptions.CurveLinear,
animations: {
destinationViewController.view.transform = CGAffineTransformIdentity
blurredBackgroundImageView.alpha = 1.0
backgroundImageView.alpha = 1.0;
},
completion: { (fininshed: Bool) -> () in
// Remove from temp super view
destinationViewController.view.removeFromSuperview()
sourceViewController.presentViewController(destinationViewController, animated: false, completion: nil)
}
)
}
}
Thanks.