I have the following small function wich contains two primary actions which I would like to occur. A three second animation, and then upon completion of that animation, another function call. What is happening, however, is that the line of code that calls another function is being executed before the UIView animation has completed.
func switchToNormalState() {
print("Switching to Normal State")
currentState = "normal"
intervalCounter = intervalCounter + 1
setVisualsForState()
UIView.animateWithDuration(Double(normalDuration), animations: { () -> Void in
self.ProgressBar.setProgress(1.0, animated: true)
}, completion: { _ in
self.switchToFastState()
})
}
I have studied other similar posts looking for the solution but none of the solutions have solved my puzzle. What am I doing wrong in the completion block in order to make these actions a sequence and not simultaneous?
Assuming that ProgressBar is a UIProgressView, the setProgress method animates itself and doesn't need to be contained in a UIView.animateWithDuration call.
This means the setProgress animation is probably longer than the duration you're putting in the UIView.animateWithDuration call, so self.switchToFastState() is being called when the UIView animation ends- before the setProgress animation completes.
Solutions
Change the duration. You can hack around this by finding/guessing the duration of the setProgress animation and using that as your UIView animation duration. This should mimic the behavior you are going for.
Change the duration but use GCD. This is the same idea as above, but doesn't use the UIView animation method. You can see this question for how to use GCD.
NSTimer or Subclassing. There are solutions here that use NSTimer or subclass UIProgressView.
I am not sure if this is the best solution, but this is what I ended up going with:
ProgressBar.progress = 1.0
UIView.animateWithDuration(Double(normalDuration), animations: { () -> Void in
self.ProgressBar.layoutIfNeeded()
}, completion: { (_) -> Void in
self.switchToFastState()
})
This solution removes the setProgress (which may be animated) from the animation block and this allows the UIView animation to execute without interuption.
Related
I have a collectionView which I need to animate height changes for, so it expands up from the foot of the screen, & back down again. It's hard to explain what's going wrong so I've attached a recording of the simulator. The animation is fine for expanding, but very odd for the collapse - the content partially disappears as it animates, then reappears when it's done. The animation code is triggered during the scrollView 'scrollViewDidScroll(_ scrollView: UIScrollView)' delegate method, with logic to determine whether to collapse or expand. The expand animation, whose code is pretty much identical, works fine.
Animation code -
private func collapseTable() {
guard self.isTableChangingSize == false else { return } // as the delegate method is triggered during the animation, we use a flag to prevent multiple collapse calls
guard self.tableHeightConstraint.constant != self.tableCollapsedHeight else { return } // don't collapse if we're already collapsed
self.isTableChangingSize = true
self.tableHeightConstraint.constant = self.tableCollapsedHeight
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
}) { _ in
self.isTableChangingSize = false
}
}
Here are a couple of videos of what's happening -
Normal speed
Slow motion
After far too long messing about trying different approaches, & tweaking everything, it seems that the problem was allowing scrolling during the animation block. If you set -
self.collectionView.isScrollEnabled = false
before the animation block, and -
self.collectionView.isScrollEnabled = true
in the completion, all is good.
I create a local var in a awakeFromNib function, use it inside a UIView animation block, but it is never released, why?
Here is the code (inside awakeFromNib in a UITableViewCell)
var fullPhotoDisposeBag = DisposeBag()
fullScreenImageView.rx.tapGesture().when(UIGestureRecognizerState.recognized).subscribe(onNext: { (tap) in
UIView.animate(withDuration: 0.15, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
fullScreenImageView.frame = oldFullScreenFrame
}, completion: { (finished) in
fullScreenImageView.removeFromSuperview()
let _ = fullPhotoDisposeBag //This line avoid early release, but retains too much...
})
}, onDisposed:{
print("disposed")
}).addDisposableTo(fullPhotoDisposeBag)
One clue is that the tableView is at the root of one tab of my app, so the UITableView is never deallocated thus the UITableViewCell is never deallocated due to the reusability.
But why would ARC keeps that variable? Used only in a function and a completion block?
PS: I currently use an optional DisposeBag that I set to nil in the completion block, but I don't understand why I need to do that...
Looks like a retain cycle. My guess is the garbage collector won't release the local variable until the completion closure stops referencing it, and the completion closure is never released because it is owned by your local function.
My advise would be to move away this code from your UIView to your UIViewController, since that is where this sort of behavior should be defined.
I'm trying to change the color of a button's background using the cross dissolve animation, however, whenever I run this it instantly change the color instead of having a smooth transition.
UIView.transition(with: self.SignIn,
duration:3,
options: UIViewAnimationOptions.transitionCrossDissolve,
animations: { self.SignIn.backgroundColor? = UIColor(hexaString: "#" + hex) },
completion: nil)
Instead of setting backgroundColor in animations block I have set it before the view transition and then start the transition of view and it will work perfectly.
self.btnAnimate.backgroundColor = UIColor.red
UIView.transition(with: self.btnAnimate,
duration: 3,
options: .transitionCrossDissolve,
animations: nil,
completion: nil)
Output
Note: I have set red color as backgroundColor you can set what ever color you want.
This is happening because your animations are conflicting with the default button animations.
To solve this, you can create a custom child class of UIButton:
import UIKit
class CustomButton: UIButton {
func animateButton(hex: String) {
// This block disables the default animations
UIButton.performWithoutAnimation {
// This is where you define your custom animation
UIView.transition(with: self, duration:3, options: UIViewAnimationOptions.transitionCrossDissolve, animations: {
self.backgroundColor? = UIColor(hexaString: "#" + hex) }, completion: nil)
// You must call layoutIfNeeded() at the end of the block to prevent layout problems
self.layoutIfNeeded()
}
}
}
Then to call this function:
signInButton.animateButton("ffffff") // Pass in your hex string
Of course, be sure to change the class of your loginButton on the story board from UIButton to CustomButton.
The downside to this approach is that the text is still darkened during the animation process. There may be a way to override this (I wasn't able to quickly determine how), so an alternative approach to this solution is to use a UILabel instead of a UIButton and set an on click listener.
Or another approach you might want to consider, is put a customLabel behind a UIButton and perform animations on the label when the button is clicked. Although this approach seems somewhat "hacky", the advantage is that the button is not disabled while the label is being animated, meaning you can register rapid sequential button presses (although it looks like the purpose of your button is to log the user in, in which case this probably wouldn't be necessary for you).
I want to execute a scroll animation after NSFetchedResultsController has finished animating moving a tableview row between sections.
The update of the section happens when I dismiss a view controller. This triggers the animation of the tableview which works fine. after this animation I want the tableview to scroll to the new location. However this does not work the scrolling starts before the animation has finished.
self.dismissViewControllerAnimated(true, completion: {
shoppingItem.category = self.categoryTextField.text // this line triggers the move between sections
dispatch_async(dispatch_get_main_queue(), {
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
do {
try self.coreDataStack.context.save()
} catch {
fatalCoreDataError(error)
}
})
})
I realize that the solution lies in managing the threads but I cannot figure out how to set this up correctly.
SOLUTION:
By delaying the scrolling by one second the animation works fine:
var time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), 1 * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue(), {
self.indexPathOfChangedItem = self.fetchedResultsController.indexPathForObject(shoppingItem)
self.itemsTableView.scrollToRowAtIndexPath(self.indexPathOfChangedItem, atScrollPosition: .Bottom, animated: true)
})
Note that the definition of the new indexpath, called indexPathOfChangedItem also has to be in the delayed call as this index path is not up to date until the animation triggered by the NSFetchedResultsController is completed.
The FRC isn't doing the animation, it's just observing the change and telling you, your code then tells the table view and it does the animation.
The animation takes no parameters to tell you when it's done or to specify how long it should take :-( raise a radar with Apple.
The simple option is to guess how long the animation runs for and dispatch after a delay to run your scroll after the animation. It's probably trial and error to find the right amount of time to wait, and once found it's unlikely to change / be different for other devices.
I have two states of UIView:
I have animation between them using #IBaction for button:
#IBAction func tapped(sender: UIButton) {
flag = !flag
UIView.animateWithDuration(1.0) {
if self.flag {
NSLayoutConstraint.activateConstraints([self.myConstraint])
} else {
NSLayoutConstraint.deactivateConstraints([self.myConstraint])
}
self.view.layoutIfNeeded() //additional line
}
}
Animation is working only when I add additional line:
But when I remove that line, then my UIView is updated but without any animation, it happens immediately.
Why this line makes such difference? How is it working?
As I understand it, changing a constraint doesn't update the layout of your views right away. It queues the changes, and they only take place the next time the system updates layout.
By putting layoutIfNeeded inside your animation block it forces the view changes to be applied during the animation block. Otherwise they take place later, after the animation has been set up.