How to not animate specific constraint changes with layoutIfNeeded() in Swift? - swift

I have a UIView which contains subviews. The subviews are animated with constraints changes and layoutIfNeeded() function :
for i in 1...cardViews.count - 1 {
let currentCard = cardViews[i]
let previousCard = cardViews[i-1]
// Set new offset
currentCard.topConstraint?.constant = previousCard.contentView.frame.height + configuration.expandedOffset
}
// To animate constraints's changes
UIView.animate(withDuration: TimeInterval(duration), delay: 0, options: [.curveEaseOut], animations: {
self.layoutIfNeeded()
}, completion: nil)
But when I do this, it also animate the contraints changes of the parent. Something like this :
self.Height == containerView.Height
How can I call layoutIfNeeded() to animate my subviews but not the parent ?
EDIT : The side effect :
http://gph.is/2noI3w9

You can call this inside of your UIView.animate
for i in 1...cardViews.count - 1 {
let currentCard = cardViews[i]
currentCard.layoutIfNeeded()
}
Instead of
self.layoutIfNeeded()

Related

Why does animated view start from wrong position?

I'm using auto layout to animate the elements in a stack view. I've set the leading and trailing constraints, both with a priority of 1000 (required). However, as you can see from the animated gif, the stack view is initially attached on the left but not on the right.
UIView.animate(withDuration: 0.125, delay: animationDelay, options: [.curveEaseOut],
animations: {
someView.transform = CGAffineTransform.identity
someView.alpha = 1
someView.isHidden = false
}, completion: nil)

UIViewControllerAnimatedTransitioning doesn't complete on iOS 9/10, but works iOS 11 and later?

I have a custom presentation controller which shows a UIViewController as a "bottom sheet" above the current ViewController, and allows an interactive gesture to dismiss the presented modal. The modal's modalPresentationStyle is set to .current, and the transitioningDelegate is set to my custom class during init(). The current code works perfectly on iOS 11 and 12, but appears to "freeze" the device on iOS 9 and 10.
Here is where the animation takes place. I have confirmed the presentedFrame and dismissedFrame are correct.
extension BottomSheetPresentationManager: UIViewControllerAnimatedTransitioning {
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.25
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let key = presenting ? UITransitionContextViewControllerKey.to
: UITransitionContextViewControllerKey.from
let keyV = presenting ? UITransitionContextViewKey.to
: UITransitionContextViewKey.from
let controller = transitionContext.viewController(forKey: key)!
let view = transitionContext.view(forKey: keyV)!
if presenting {
transitionContext.containerView.addSubview(view)
}
let presentedFrame = transitionContext.finalFrame(for: controller)
var dismissedFrame = presentedFrame
switch direction {
case .left:
dismissedFrame.origin.x = -presentedFrame.width
case .right:
dismissedFrame.origin.x = transitionContext.containerView.frame.size.width
case .top:
dismissedFrame.origin.y = -presentedFrame.height
case .bottom:
dismissedFrame.origin.y = transitionContext.containerView.frame.size.height
}
let initialFrame = presenting ? dismissedFrame : presentedFrame
let finalFrame = presenting ? presentedFrame : dismissedFrame
// Runs when the animation finishes
let completionBlock: (Bool) -> Void = { (finished) in
// tell our transitionContext object that we've finished animating
if transitionContext.transitionWasCancelled {
if self.interactive {
transitionContext.cancelInteractiveTransition()
}
transitionContext.completeTransition(false)
} else {
if self.interactive {
finished ? transitionContext.finishInteractiveTransition() : transitionContext.cancelInteractiveTransition()
}
transitionContext.completeTransition(finished)
}
}
// Put what you want to animate here.
let animationBlock: () -> Void = {
view.frame = finalFrame
}
// Set up for the animation
let animationDuration = transitionDuration(using: transitionContext)
view.frame = initialFrame
// Perform a different animation based on whether we're interactive (performing a gesture) or not
if interactive {
// Do a linear animation so we match our dragging with our transition
UIView.animate(withDuration: animationDuration,
delay: 0,
options: .curveLinear,
animations: animationBlock,
completion: completionBlock)
} else {
// Do a spring animation with easing
UIView.animate(withDuration: animationDuration,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.45,
options: [.curveEaseInOut],
animations: animationBlock,
completion: completionBlock)
}
}
}
After much testing, I have figured out that the completion block on the UIView.animate block never gets called (completionBlock in the code), and in the Capture View Hierarchy view, it actually shows my presented view on top, with what looks like a screenshot of the view (UIVisualEffectView) directly under the presented menu.
What you are seeing there is my bottom menu (the UITableView with cells that you see in front), with a UIVisualEffectView behind it (appears to be a screenshot of the UICollection view behind that), and a UICollectionView that called the present() method for the menu.
What would cause the UIView.animate block not to complete? Is there any reason the behavior would differ between iOS 10 and 11? Any help would be greatly appreciated.

Updating bottom constraint with SnapKit and Swift 4

Here is my code. I am using KeyboardHelper library for managing keyboard states and getting keyboard height, and also SnapKit. I have a text field that triggers keyboard. When a keyboard is visible I want to raise blue box above keyboard bounds, so it's visible. So far I failed to do so. Can you please help me with my code?
private var keyboardHelper : KeyboardHelper?
let box = UIView()
override func viewDidLoad() {
super.viewDidLoad()
keyboardHelper = KeyboardHelper(delegate: self)
guard let superview = self.view else {
return
}
box.backgroundColor = UIColor.blue
superview.addSubview(box)
box.snp.makeConstraints { make in
make.bottom.equalTo(superview.snp.bottom).offset(-16)
make.width.equalTo(200)
make.centerX.equalTo(superview)
make.height.equalTo(100)
}
}
func keyboardWillAppear(_ info: KeyboardAppearanceInfo) {
UIView.animate(withDuration: TimeInterval(info.animationDuration), delay: 0, options: info.animationOptions, animations: {
self.box.snp.updateConstraints({ make in
make.bottom.equalTo(info.endFrame.size.height).offset(10)
})
self.box.layoutIfNeeded()
}, completion: nil)
}
According to SnapKit docs:
if you are only updating the constant value of the constraint you can use the method snp.updateConstraints
You need to use snp.remakeConstraints in your case. Furthermore, you should probably call layoutIfNeeded on superview.
Optionally you could get rid of changing constraints and just animate proper transforms on show:
box.transform = CGAffineTransform(translationX: 0.0, y: neededTranslation)
on hide:
box.transform = .identity

UICollectionView cell. trying to animate subviews

trying to animate a subview inside a collection view cell but am only getting an abrupt change between states.
...
func animate (){
if self.signOut.hidden == false{
UIView.animateWithDuration(0.2) {
self.signOut.hidden = true
}
}else{
UIView.animateWithDuration(0.2) {
self.signOut.hidden = false
}
}
...
any tips much appreciated!
You need to decrease the alpha inside an animation block for the view to disappear smoothly
UIView.animateWithDuration(0.33, delay: 0.0, options: [.CurveEaseInOut], animations: {
self.signOut.alpha = 0.0
}) { finished in
self.signOut.hidden = true
}

Weird UIVisualEffectView behavior in multitasking

I have an AccountsViewController which has UIVisualEffectView with blur effect in background (subview at index 0), so that it covers previous controller with blur during transition. But when I then switch to another app and open multitasking menu again to switch back to my app, its blurry UIVisualEffectView seem to be damaged (as shown on the screenshot 1). When I make my app active again, the view gets "repaired" and looks ok again (as shown on the second screenshot)
Some code from my custom transitioning class where I add the blur view
// in the animateTransition.. method
guard let container = transitionContext.containerView() else {
return
}
let blurEffectView: UIVisualEffectView
if container.viewWithTag(101) != nil {
blurEffectView = container.viewWithTag(101)! as! UIVisualEffectView
}
else {
blurEffectView = UIVisualEffectView(frame: container.frame)
blurEffectView.translatesAutoresizingMaskIntoConstraints = false
blurEffectView.tag = 101
container.addSubview(blurEffectView)
}
if self.isPresenting {
// We're presenting a view controller over another
toViewController.view.transform = CGAffineTransformMakeScale(0.7, 0.7)
container.addSubview(toViewController.view)
container.sendSubviewToBack(blurEffectView)
toViewController.view.alpha = 0
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [], animations: { () -> Void in
fromViewController.view.transform = CGAffineTransformMakeScale(0.8, 0.8)
toViewController.view.transform = CGAffineTransformMakeScale(1, 1)
blurEffectView.effect = UIBlurEffect(style: .Dark)
toViewController.view.alpha = 1
}, completion: { (completed) -> Void in
self.context?.completeTransition(completed)
})
}
else {
// We're dismissing a view controller
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [], animations: { () -> Void in
fromViewController.view.transform = CGAffineTransformMakeScale(0.7, 0.7)
toViewController.view.transform = CGAffineTransformMakeScale(1, 1)
blurEffectView.effect = nil
fromViewController.view.alpha = 0
}, completion: { (completed) -> Void in
fromViewController.view.removeFromSuperview()
self.context?.completeTransition(completed)
})
}
Seems like I've figured it out myself: this issue appears only when there is an UIWindow back (which is black) showing through.
In this question I had a transition animation container with these views:
fromViewController's view (with a scale transform 0.8;0.8)
UIVisualEffectView with .Dark style blur applied to it
toViewController's view
Because of fromViewController's view size (which was smaller than the screen because of scale transform), the black UIWindow view was showing through. And according to my research this causes UIVisualEffectView to produce a glitch described in this question.
The solution is to add a black UIView with the frame of an UIWindow and add it to the bottom of the container's view hierarchy:
let blackView = UIView(frame: container.frame)
blackView.backgroundColor = UIColor.blackColor()
container.insertSubview(blackView, atIndex: 0)
And then this glitch seem to appear no longer.