32

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
    })
}
Blazej SLEBODA
  • 8,936
  • 7
  • 53
  • 93
Dru Freeman
  • 1,766
  • 3
  • 19
  • 41
  • "The view is added to the subview at the time of viewDidLoad" <- let's see that code. – Graham Perks Apr 05 '16 at 20:41
  • 1
    FWIW, you can pass 'nil' as the completion blocks here. Even if you do have blocks, they certainly shouldn't be returning anything. I'm surprised the Swift compiler didn't flag that as an error. – Graham Perks Apr 05 '16 at 20:44
  • @GrahamPerks, I added all relevant code above. I'd tried it with and without the return as shown in the original pre-tweaked failing code. – Dru Freeman Apr 06 '16 at 12:15
  • Lastly. The view appears and configures just fine. Everything is where it's supposed to be. All UI elements work, and rotating adjusts contents by constraints. The ONLY issue is that the alpha change is not occurring as a fade. Simply an instant appear. – Dru Freeman Apr 06 '16 at 12:16
  • What is AFLog ? Are you sure to be in the main during the animation ? – Max Apr 08 '16 at 13:30
  • AFLog is merely my wrapper for print("Class:Function <<< Block") – Dru Freeman Apr 09 '16 at 01:32
  • 2
    Your code looks correct. I've seen unexpected UI behavior like this when there are threading issues elsewhere in the app. Try placing a breakpoint on UIView.animateWithDuration() and self.infoDetailView.alpha =... Are those both being executed on Thread 1 (com.apple.main-thread)? – GingerBreadMane Apr 11 '16 at 22:13
  • one common gotchya is that you have to call layoutIfNeeded **on the superview**, not on the view. – Fattie Nov 24 '17 at 13:01
  • For others with similar issues, be sure to check out the answers on [this question](https://stackoverflow.com/questions/3762561/how-to-animate-the-background-color-of-a-uilabel) -- not all backgrounds can be animated. UIView's background can, UILabel's can't. Interestingsly, myLabel.layer.backgroundColor **can** be animated -- just not myLabel.backgroundColor. Go figure. – ConfusionTowers Mar 20 '18 at 15:51

11 Answers11

70

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.

Karen Hovhannisyan
  • 1,140
  • 2
  • 21
  • 31
Darko
  • 9,655
  • 9
  • 36
  • 48
15

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.

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • Sulthan, this streamlined the code nicely, but didn't affect the root problem of the animation. The infoDetailView is only declared as shown above as the IBOutlet for the View that is on the scene dock for the storyboard scene. Is there any potential IB Toggles for either the docked view or the storyboard scene that might need to be fixed? – Dru Freeman Apr 09 '16 at 01:47
  • @LordAndrei There is nothing else wrong with your code so I am pretty sure it's some other hidden issue somewhere else. There are too many options. If you could reproduce the issue in a sample project and share for us, then we could help you. – Sulthan Apr 11 '16 at 20:04
12

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
    }
}
spencer.sm
  • 19,173
  • 10
  • 77
  • 88
4

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

Community
  • 1
  • 1
3

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.

Jeff
  • 166
  • 5
1

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 
})
Karen Hovhannisyan
  • 1,140
  • 2
  • 21
  • 31
shikha kochar
  • 163
  • 1
  • 7
1

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:

my storyboard and project folder photo

Every object (view and buttons) are with default settings.

enter image description here

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.

Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
  • 1
    Alessandro, was the view that you are animating taken from the scene dock on the top of the storyboard scene? – Dru Freeman Apr 09 '16 at 01:45
  • The view with green background is "infoDetailView", a view added , in my example, a the center of a clean viewController, and two buttons...make all connections (iboutlet and ibactions) , set autolayout and it work. – Alessandro Ornano Apr 09 '16 at 07:43
  • I've update my answer with the source code and the storyboard, you can start new project and try it. – Alessandro Ornano Apr 13 '16 at 16:53
1

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)
}
Arun Gupta
  • 2,628
  • 1
  • 20
  • 37
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.
BartoszKP
  • 34,786
  • 15
  • 102
  • 130
Rudi Wever
  • 11
  • 3
0

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;
})
Wanpaya
  • 49
  • 6
0

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.

Sasha Kolsky
  • 432
  • 1
  • 5
  • 14