Scenario: I have a '>' button:
that is supposed to animate +90 degrees upon initial tap:
However after returning to the 0 degree '>' position via tapping on the UITableViewCell again which returns to its original height; then tapping AGAIN I get a further rotation to the '<' position:
How do I freeze the max rotation to only the 90 degrees (pointing down); so that I have the single +/- 90 deg rotation toggle?
Here's my code:
func rotate2ImageView() {
UIView.animate(withDuration: 0.3) {
self.rightArrowImage.transform = self.rightArrowImage.transform.rotated(by: .pi / 2)
}
}
Here's a failed remedy (where I tried to remove all animations):
func rotateImageView(){
UIView.animate(withDuration: 0.3, animations: {() -> Void in
self.rightArrowImage.transform = self.rightArrowImage.transform.rotated(by: .pi / 2)
}, completion: {(_ finished: Bool) -> Void in
if finished {
self.rightArrowImage.layer.removeAllAnimations()
}
})
}
I needed to reset the '>' image's transformation prior to future taps to rotate the icon. I did this via setting its transformation to 'identity' per Matt's suggestion.
func rotateImageView() {
// Reset any pending transformation to original status:
rightArrowImage.transform = .identity
UIView.animate(withDuration: 0.3) {
self.rightArrowImage.transform = self.rightArrowImage.transform.rotated(by: .pi / 2)
}
}
This is fired every via the UITableViewDelete's willDisplay function:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let myCell = cell as? CommonFAQCell
if expandCell {
myCell?.rotateImageView()
}
}
I want to scroll to a given index (self.boldRowPath), but when I debug scrollToRow is performed before reloadData().
How to know reloadData has finished ?
func getAllTimeEvent() {
self.arrAllTimeEvent = ModelManager.getInstance().getAllTimeEvent(from: self.apportmentDateFrom, to: self.apportmentDateTo)
self.tblTimeEvent.reloadData()
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
}
Any help will be much appreciated.
Try doing this, it should work:
self.tblTimeEvent.reloadData()
DispatchQueue.main.async(execute: {
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
})
This will execute the scrollToRow on the main thread, that means after the reloadData is done (because it is on the main thread)
As explained in this answer, the reload of the UITableView happens on the next layout run (usually, when you return control to the run loop).
So, you can schedule your code after the next layout by using the main dispatch queue. In your case:
func getAllTimeEvent() {
self.arrAllTimeEvent = ModelManager.getInstance().getAllTimeEvent(from: self.apportmentDateFrom, to: self.apportmentDateTo)
self.tblTimeEvent.reloadData()
DispatchQueue.main.async {
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
}
}
You can also force the layout by manually calling layoutIfNeeded. But this is generally not a good idea (the previous option is the best):
func getAllTimeEvent() {
self.arrAllTimeEvent = ModelManager.getInstance().getAllTimeEvent(from: self.apportmentDateFrom, to: self.apportmentDateTo)
self.tblTimeEvent.reloadData()
self.tblTimeEvent.layoutIfNeeded()
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
}
you can make sure reload is done using...
In Swift 3.0 + we can create a an extension for UITableView with a escaped Closure like below :
extension UITableView {
func reloadData(completion: #escaping () -> ()) {
UIView.animate(withDuration: 0, animations: { self.reloadData()})
{_ in completion() }
}
}
And Use it like Below where ever you want :
Your_Table_View.reloadData {
print("reload done")
}
hope this will help to someone. cheers!
You can make a pretty solid assumption that a UITableView is done reloading when the last time tableView(_:willDisplay:forRowAt:) is called for the visible sections on the screen.
So try something like this (for a tableView where the rows in section 0 take up the available space on the screen):
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastRowIndex = tableView.numberOfRows(inSection: 0)
if indexPath.row == lastRowIndex - 1 {
// tableView done reloading
self.tblTimeEvent.scrollToRow(at: self.boldRowPath ?? [0,0], at: .top, animated: true)
}
}
You can use CATransaction for that
CATransaction.begin()
CATransaction.setCompletionBlock {
// Completion
}
tableView.reloadData()
CATransaction.commit()
tableView.reloadData { [weak self] in
self?.doSomething()
}
Note: I’ve already checked the following stack overflow issues:
27907570, 32229252, 26118141, 31604300
All I am trying to do is fade animate in a view (by alpha) when called by an IBAction attached to a button. Then reverse when a button on the view is hit.
My wrinkle may be that I'm using a secondary view that is on the ViewDock in the storyboard View. The view is added to the subview at the time of viewDidLoad where the frame/bounds are set to the same as the superview (for a full layover)
The reason this is done as an overlay view since it is a tutorial indicator.
The result (like many others who've listed this problem) is that the view (and contained controls) simply appears instantly and disappears as instantly. No fade.
I have tried animationWithDuration with delay, with and without completion, with transition, and even started with the old UIView.beginAnimations.
Nothing is working. Suggestions warmly welcomed.
The code is about as straight forward as I can make it:
Edit: Expanded the code to everything relevant
Edit2: TL;DR Everything works with the exception of UIViewAnimateWithDuration which seems to ignore the block and duration and just run the code inline as an immediate UI change. Solving this gets the bounty
#IBOutlet var infoDetailView: UIView! // Connected to the view in the SceneDock
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
view.addSubview(infoDetailView)
updateInfoViewRect(infoDetailView.superview!.bounds.size)
}
func updateInfoViewRect(size:CGSize) {
let viewRect = CGRect(origin: CGPointZero, size: size)
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
updateInfoViewRect(size)
}
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.0
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.75
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
// MARK: - IBActions
#IBAction func openInfoView(sender: UIButton) {
showInfoView()
}
#IBAction func closeInfoView(sender: UIButton) {
hideInfoView()
}
Please note, I started with the following:
func showInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.75
})
}
func hideInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.00
})
}
If you infoDetailView is under auto layout constraints you need to call layoutIfNeeded on the parent view inside animateWithDuration:
func showInfoView() {
self.view.layoutIfNeeded() // call it also here to finish pending layout operations
UIView.animate(withDuration: 2.0, animations: {
self.infoDetailView.alpha = 0.75
self.view.layoutIfNeeded()
})
}
Theoretically this should not be needed if you just change the .alpha value, but maybe this could be the problem in this case.
There are several strange things I can see,
first, remove:
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
Usually you don't need to call those methods manually unless you know exactly what you are doing.
Also, when you are changing the size:
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
You never need to set both bounds and frame. Just set frame.
Also, you should probably make sure that the view actually doesn't ignore the frame by setting:
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
Instead of resetting the frame, just set autoresize mask:
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
Resulting in:
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
infoDetailView.frame = view.bounds
view.addSubview(infoDetailView)
}
func hideInfoView() {
...
}
I think this should actually help because immediate animations are often connected to size problems.
If the problem persists, you should check whether the infoDetailView in your animation is the same object as the infoDetailView you are adding to the controller.
For others looking to start an animation immediately when a view loads...
The animation won't work if you call UIView.animate(...) inside viewDidLoad. Instead it must be called from the viewDidAppear function.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 3) {
self.otherView.frame.origin.x += 500
}
}
If the animation does not seem to execute then consider examining the state of each of your views, before you enter the animation block. For example, if the alpha is already set to 0.4 then the animation that adjusts your view alpha, will complete almost instantly, with no apparent effect.
Consider using a keyframe animation instead. This is what a shake animation in objective c looks like.
+(CAKeyframeAnimation*)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.values = #[[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-10.0, 0.0, 0.0)],
[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(10.0, 0.0, 0.0)]];
animation.autoreverses = YES;
animation.repeatCount = 2;
animation.duration = 0.07;
return animation;
}
Here is a post that shows you how to adjust alpha with keyframes https://stackoverflow.com/a/18658081/1951992
Make sure infoDetailView's opaque is false.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/UIView/opaque
This property provides a hint to the drawing system as to how it should treat the view. If set to true, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance. If set to false, the drawing system composites the view normally with other content. The default value of this property is true.
Try Below code. Just play with alpha and duration time to perfect it.
Hide func
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.8
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.4
},
completion:
{ (finished) in
self.infoDetailView.alpha = 0.0
}
)
}
)
AFLog.exit(thisClass)
}
Show func
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.3
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.7
},
completion:
{ (finished) in
self.infoDetailView.alpha = 1.0
}
)
}
)
AFLog.exit(thisClass)
}
I've replicated your code and it work well, it's all ok.
Probably you must control constraints, IBOutlet and IBActions connections. Try to isolate this code into a new project if it's necessary.
Update: my code
and my storyboard and project folder photo:
Every object (view and buttons) are with default settings.
I've commented all AFLog lines (probably it's only any more "verbose mode" to help you) , the rest of your code is ok and it do what do you aspected from it, if you press open button the view fade in, and when you tap close button the view fade out.
PS Not relevant but i'm using xCode 7.3 , a new swift 2.2 project.
Use this code:
Swift 2
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Swift 3, 4, 5
UIView.animate(withDuration: 0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Have you tried changing your showInfoView() to something more like toggleInfoView?
func toggleInfoView() {
let alpha = CGFloat(infoDetailView.alpha == 0 ? 1 : 0)
infoDetailView.alpha = alpha //this is where the toggle happens
}
It says that if your view's alpha is 0, then change it to 1. Else, make it 0.
If you need that to happen in an animation, try
#IBAction func openInfoView(sender: UIButton) {
UIView.animate(withDuration: 2.0, animations: {
self.toggleInfoView() //fade in/out infoDetailView when animating
})
}
You'll still want to keep that infoDetailView.alpha = 0.0 where you have it, coming from the viewDidLoad.
For UILabel component try to changes layer's background color instead.
Try this (Tested on Swift 4):
UIView.animate(withDuration: 0.2, animations: {
self.dateLabel.layer.backgroundColor = UIColor.red.cgColor;
})
Had a similar issue with animation not being performed.
Changed the function call use perform(aSelector: Selector, with: Any?, afterDelay: TimeInterval) in the form of perform(#selector(functionThatDoesAnimationOfAlphaValue), with: nil, afterDelay: 0) and it worked. Even with a TimeInterval set to 0.
In case someone else comes here wondering for a solution.
I have a UIScrollview with buttons that I use for paging right and left:
#IBAction func leftPressed(sender: AnyObject) {
self.scrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
}
I'd like to perform an action after the scrollview has finished the paging animation. Something like:
#IBAction func leftPressed(sender: AnyObject) {
self.scrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
secondFunction()
}
The above code doesn't work because the second function runs before the scrollview is finished animating the offset. My initial reaction was to use a completion handler but I'm not sure how to apply one to the setContentOffset function. I've tried:
func animatePaging(completion: () -> Void) {
self.mainScrollView!.setContentOffset(CGPointMake(0, 0), animated: true)
completion()
}
with the call
animatePaging(completion: self.secondFunction())
But I get the error "Cannot invoke 'animatePaging' with an argument list of type '(completion())'. Any thoughts?
The problem is that you need a completion handler for the scrolling animation itself. But setContentOffset(_:animated:) does not have a completion handler.
One solution would be that you animate the scrolling yourself using UIView's static function animateWithDuration(_:animations:completion:). That function has a completion handler that you can use:
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.scrollView.contentOffset = CGPointMake(0, 0)
}) { (finished) -> Void in
self.secondFunction()
}
Update from joern answer - Swift 4.2
UIView.animate(withDuration: 0.5, animations: { [unowned self] in
self.scrollView.contentOffset = .zero
}) { [unowned self] _ in
self.secondFunction()
}
I have a UITableViewController which allows multiple selection and it presents model of the data stored in the CoreData.
When user taps on the row we need to animate it to selected state (change layout, fade in some elements etc). The problem is that when user taps on row and makes it selected - model gets updated (because we store selected items in the model too). Because of the NSFetchedResultController aborts our fancy animations as whole table gets reloaded.
To make it more clear. Here is configureCell method i call in tableView:cellForRowAtindexPath. The reason to have setSelected method with animated: false there is to set already selected cells to a proper state when user scrolls the table.
func configureCell(cell: MealCell, indexPath: NSIndexPath) {
let menuItem = self.fetchedResultsController.objectAtIndexPath(indexPath) as! MenuItem
cell.name = menuItem.name
// some cell initialisation
//we have this code to draw cells selected when user scrolls our table view. We don't need animation here.
if menuItem.isSelected {
cell.setSelected(true, animated: false)
} else {
cell.setSelected(false, animated: false)
}
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
}
All animations happen in setSelected method of the MealCell class
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
extraView.hidden = !selected
let extraViewAlpha: CGFloat = selected ? 1.0 : 0.0
self.extraViewWidthConstraint.constant = selected ? 38 : 0
if animated {
UIView.animateWithDuration(0.5, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
self.layoutIfNeeded()
}, completion: { completed -> Void in
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.extraView.alpha = extraViewAlpha
})
})
}
else {
self.layoutIfNeeded()
self.extraView.alpha = extraViewAlpha
}
}
The best is to change your way of storing selectedItems to prevent the fetchedResultsController from notifying the delegate of the changes. a workaround is to temporary set the fetchedResultsController.delegate to nil and set it back after animation ends.