As you can see I'm having trouble formulating the question. Let me try to explain:
I'm using a search bar in my swift ios app. In order to get a desired animation effect I put it in a vertical stack view and then animate its isHidden property. This way search bar pushes down other children of the stack view as it animates in or pulls them up as it animates out. So far so good.
I've noticed a behavior that I think is strange. Could be a bug or could be me not understanding how things work. Basically if I call search bar hiding method x times in a row I need to call search bar showing method x times before it would show. I'd expect to have to make just one call to show search bar regardless of how many times I called hiding method. The issue doesn't exist other way around: if I call search bar showing code x times I only need to call hiding method once for it to go away. This doesn't happen if I set isHidden without animating it...
Here's a sample code and a video of the issue. I'd appreciate it if someone would help me understand this behavior.
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar! {
didSet {
searchBar.isHidden = true
}
}
#IBAction func showAction(_ sender: UIButton) {
expandSearch()
}
#IBAction func hideAction(_ sender: UIButton) {
collapseSearch()
}
private func expandSearch() {
UIView.animate(withDuration: 0.3){
self.searchBar.isHidden = false
}
}
private func collapseSearch() {
UIView.animate(withDuration: 0.3){
self.searchBar.isHidden = true
}
searchBar.resignFirstResponder()
}
}
You should not call an asynchronous animation of searchbar x times, instead of I suggest you to keep its state in variable, something like isSearchBarHidden,
and check it before show/hide search bar. You could use just one method with such signature showSearchBar(show: Bool) and setting this variable there.
#IBAction func showAction(_ sender: UIButton) {
showSearchBar(true)
}
#IBAction func hideAction(_ sender: UIButton) {
showSearchBar(false)
}
private
func showSearchBar(_ show: Bool) {
guard isSearchBarHidden != show else {
return
}
UIView.animate(withDuration: 0.3, animations: {
self.searchBar.isHidden = show
}) {
self.isSearchBarHidden = show
if !show && searchBar.isFerstResponder {
searchBar.resignFirstResponder
}
}
}
private
var isSearchBarHidden: Bool = true
Also it is a good practice to check if your textView/textField/searchBar isFirstResponder before call resignFirstResponder.
Hope it will help. Good luck!
I'm running into a weird situation where animating a UIImageView's alpha affects a UIButton which also exists on the same view.
My code:
func handleArrowAnimation(_ arrowImage: UIImageView, _ arrowImageXCenterConstraint: NSLayoutConstraint) {
arrowImageXCenterConstraint.constant = CGFloat(80)
UIView.animate(withDuration: 0.7, delay: 0, options: [.curveEaseInOut, .repeat, .autoreverse], animations: {
arrowImage.alpha = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? 1 : 0.2
arrowImage.superview!.layoutIfNeeded()
}) { (completed) in
arrowImageXCenterConstraint.constant = CGFloat(0)
arrowImage.alpha = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? 0.2 : 1
arrowImage.superview!.layoutIfNeeded()
}
}
The result:
I found that removing the call to layoutIfNeeded() prevents the UIButton alpha from changing, but of course it also prevents the arrow from moving - so it doesn't help me much.
The UIButton is not a subview of the arrowImage, and they don't share the same parent view (their parents share the same parent, though).
What am i missing here?
Thanks!
So apparently someone else had this issue and the answer is to make sure you start your animations after the view has loaded, for example in the viewDidAppear() method.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear()
handleArrowAnimation()
}
Here is the link to previous question. There does not appear to be any explanation for the strange behaviour at this time.
I am using this code for periscope-style comments in my iOS app (where the comment bubbles slide up from the bottom): https://github.com/yoavlt/PeriscommentView
And this is the code that actually animates the comments in and out:
public func addCell(cell: PeriscommentCell) {
cell.frame = CGRect(origin: CGPoint(x: 0, y: self.frame.height), size: cell.frame.size)
visibleCells.append(cell)
self.addSubview(cell)
UIView.animateWithDuration(self.config.appearDuration, delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
let dy = cell.frame.height + self.config.layout.cellSpace
for c in self.visibleCells {
let origin = c.transform
let transform = CGAffineTransformMakeTranslation(0, -dy)
c.transform = CGAffineTransformConcat(origin, transform)
}
}, completion: nil)
UIView.animateWithDuration(self.config.disappearDuration, delay: self.config.stayDuration, options: UIViewAnimationOptions.CurveEaseIn, animations: { () -> Void in
cell.alpha = 0.0
}) { (Bool) -> Void in
self.visibleCells.removeLast()
cell.removeFromSuperview()
}
}
The problem with the above code is that sometimes when a new comment is added, it shows up overlapping the previous comment. The expected behavior is that the previous comment slides up and the new comment takes its place. I noticed that this mainly happens when you add a new comment after the previous comment starts to fade out, but still has not disappeared.
I tried putting a breakpoint in the self.visibleCells.removeLast(), and it does seem like this gets called only when the last comments completed disappears, so I would expect this to work correctly (because the for loop moves up all the visible cells, and even when a comment is fading out, it is still visible).
Any help with this would be appreciated.
Thanks!
I just got a clone of that repository, run on my device and your problem is trying to rewrite the functionality. Do not do that. Instead, just add this line:
#IBAction func addNewCell(sender: UIButton) {
self.periscommentView.addCell(UIImage(named: "twitterProfile")!, name: "Your_Name_Here", comment: "Your_Comment_Here")
}
That's all! I just checked it and it works perfectly!
Do not try to change alpha or moving up. The library does all of these stuffs! Good luck! ;)
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 used the code shown below to present a new view controller in a seamless sliding motion. The actual animation works perfectly but when the animation has finished, all of the views disappear and i am left with a blank screen. There is an error posted in the console saying
"Unbalanced calls to begin/end appearance transitions for < Rocket_Game18GameViewController: 0x155e288f0 >"
class SlideRightToLeft: UIStoryboardSegue {
override func perform() {
var sourceVC:UIViewController = self.sourceViewController as UIViewController
var destVC:UIViewController = self.destinationViewController as UIViewController
let sourceVCFrame = sourceVC.view.frame
let width = destVC.view.frame.size.width
sourceVC.view.addSubview(destVC.view)
destVC.view.frame = CGRectOffset(sourceVCFrame, width, 0)
UIView.animateWithDuration(1.5, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
sourceVC.view.frame = CGRectOffset(sourceVCFrame, -width, 0)
}, completion: {
finished in
destVC.view.removeFromSuperview()
sourceVC.presentViewController(destVC, animated: false, completion: nil)
})
}
}
Please ask if you need any more information, Thanks!
I have now figured out what i was doing wrong by converting someone else's objective-c code into swift. It turns out that my whole custom animation idea was wrong and when iOS7 was released, they changed how it was done. Here is the link to the blog where i found the solution incase anyone else would like to see it
https://github.com/jbradforddillon/TransitioningExample