viewWillLayoutSubviews messing with my animation

I want to click a button and have my View slowly animate into the viewing area from the right side of the screen. Then click a second button and have the view animate off the screen from left to right


I added a Button (OpenButton with IBAction = openView) and a View (DataView) into my main ViewController. I set constraints on DataView to be the safe area for top(20), bottom(20), leading(16) and trailing(16). I also added a Button (CloseButton with IBAction = closeView) into the DataView.


While in Portrait mode, clicking the OpenButton results in the OpenView( ) function being called and the DataView animating properly from right to left into the visible screen as desired. Clicking the CloseButton results in the view animating off the screen from left to right as desired. (Note: There are no calls to viewWillLayoutSubviews( ) which are made by the system at this time). I can click OpenButton and CloseButton multiple times and the animation works great each time while in Portrait mode.


I now switched to Landscape mode. Clicking the OpenButton results in the openView( ) function being called and the DataView animating properly from right to left into the visible screen as desired. Clicking the CloseButton results in weird animation in the wrong places and the DataView does not properly go off the screen. Debugging has shown that when the CloseButton is pressed, then viewWillLayoutSubviews() and viewDidLayoutSubviews( ) are called, then the closeView( ) function is called and then viewWillLayoutSubviews() and viewDidLayoutSubviews( ) are called a second time. When viewDidLayoutSubviews() is called a second time, then the DataView snaps back to it's origin.x constraint value of 16 which results in it moving back to the leading safe area, which affects the animation which is currently going on. It seems like that is what is messes up the animation.


Debug "print" statement output


<Note: OPEN button clicked here>

openScoreWindow -- enter

dataView.frame.origin.x = 16.0

animation complete - openScoreWindow


<Note: CLOSE button clicked here>

viewWillLayoutSubviews

dataView.frame.origin.x = 16.0

viewDidLayoutSubviews

dataView.frame.origin.x = 16.0


closeScoreWindow -- enter

dataView.frame.origin.x = 16.0

viewWillLayoutSubviews

dataView.frame.origin.x = 767.0 <--- this value is set in the UIVIew_Animate( ) in the closeView( ) function, as desired

viewDidLayoutSubviews

dataView.frame.origin.x = 16.0 <--- this is proof the "leading" constraint is reset back to 16 during the active animation


animation complete - closeScoreWindow . <-- this is when animation has completed



** I tried to replicate this scenario by creating a separate brand new swift "test project" which just had the two buttons and dataView, however, in this "test project", the animation in portrait and landscape mode works as desired since viewWillLayoutSubviews() is never called when opening and closing the view. Oh well, for some reason, in my project, viewWillLayoutSubviews() is called twice when I click the Close button.


I have no idea how to get around this issue. Any ideas?


override func viewDidLoad()
{
      super.viewDidLoad()
      dataView.isHidden = true

     print ("viewDidLayoutSubviews")
     print("   dataView.frame.origin.x = \(dataView.frame.origin.x)")
     print("")
}

@IBAction func openView()
{
     print("openScoreWindow -- enter")
     print("   dataView.frame.origin.x = \(dataView.frame.origin.x)")

      //ensure view is positoned out of the viewing area
      dataView.frame.origin.x = getScreenSize().width + 100

       //make view visible for eventual animation
       dataView.isHidden = false

        //animate view moving from right to left onto the screen
        UIView.animate(withDuration: 1.0, //1 second
            delay: 0,
            options: .curveEaseInOut,
            animations:
            {
                self.dataView.frame.origin.x = 16
            },
            completion: { [weak self] finished in
                print("   animation complete - openScoreWindow")
                print("")
            })
    }
}


    @IBAction func closeView()
    {
        print("closeScoreWindow -- enter")
        print("   dataView.frame.origin.x = \(dataView.frame.origin.x)")

        //animate view moving from left to right off the screen
        UIView.animate(withDuration: 3.0, //1 second
            delay: 0,
            options: .curveEaseInOut,
            animations: {
                //move view out of the viewing area
                self.dataView.frame.origin.x = self.getScreenSize().width + 100
            },
            completion: { [weak self] finished in
                //hide the view
                self?.dataView.isHidden = true

                print("   animation complete - closeScoreWindow")
                print("")
           })
    }

Accepted Reply

*** I FOUND A SOLUTION THAT WORKS !!! ***


I continued to google around and found a couple of posts.


The first post indicated that if you have a constraint assigned to a view, that you want to animate, then you should be animating the view by changing the constraint values instead of the origin.x or origin.y values.


As a result I decided the create an IBOutlet for the leading constraint to the dataView (ie: viewLeadingAlignment_Constraint) and set it accordingly inside the openView and closeView functions. I guess since I am now using a "constraint value" for animation, then when the viewWillLayoutSubview( ) call occurs, while animation is active, then the view still maintains it's current constraint value location and no longer gets reset back to the 16 constraint I had before.


Anyway, I ran the program and the dataView appears and disappears without any animation at all, so this did NOT work.

However, please read more below ...


The second post indicated the following "Two important notes":


  1. You need to call
    layoutIfNeeded
    within the animation block. Apple actually recommends you call it once before the animation block to ensure that all pending layout operations have been completed
  2. You need to call it specifically on the parent view (e.g.
    self.view
    ), not the child view that has the constraints attached to it. Doing so will update all constrained views, including animating other views that might be constrained to the view that you changed the constraint of (e.g. View B is attached to the bottom of View A and you just changed View A's top offset and you want View B to animate with it)


Based on these two notes, I added calls to self.view.layoutIfNeeded( ) after each line where I set the IBOutlet constraint value inside the openView and closeView functions and then everything worked as desired (see updated/working code below). I can now open/close the dataView in portrait and landscape modes with expected animation.



    override func viewDidLoad()
    {
       dataView.isHidden = true
    }

    @IBAction func openView()
    {
        //ensure view is out of the viewing area
        viewLeadingAlignment_Constraint.constant = getScreenSize().width + 100
        self.view.layoutIfNeeded()

        //make view visible for eventual animation
        dataView.isHidden = false

        //animate view appearing
        UIView.animate(withDuration: 1.0,
            delay: 0,
            options: .curveEaseInOut,
            animations:
            {
                self.viewLeadingAlignment_Constraint.constant = 16
                self.view.layoutIfNeeded()
            },
            completion: nil )
      }


    @IBAction func closeView()
    {
        //animate view disappearing
        UIView.animate(withDuration: 1.0,
            delay: 0,
            options: .curveEaseInOut,
            animations: {
                //move view out of the viewing area
                self.viewLeadingAlignment_Constraint.constant = self.getScreenSize().width + 100
                self.view.layoutIfNeeded()
            },
            completion: { [weak self] finished in
                //hide view
                self?.dataView.isHidden = true
            })
    }



Based on the second posting #2 statement, perhaps my original design of moving dataView by changing the origin.x value was not working properly since the dataView "view" included other views (buttons, labels, etc..) inside of it. I was requesting animation to occur on dataView only, so the other views, which are located inside of dataView, were not part of this animation request and thus bad/weird things happened during animation, however, this is just a complete guess.

Replies

What occurs when you return to Portrait ? Do open and close work properly again ?

If so, the problem may well be in the constraints you have defined.

Could you detail them ?


What is the problem you want to avoid ?

- Having viewWillLayout called twice ?

- Having correct constraints in landscape ?

>>What occurs when you return to Portrait ? Do open and close work properly again ?

If an orientation change has never occurred then the animation works fine. As soon an an orientation occurs then the new orientation will experience this issue (see below)


Application starts in portrait mode and first animation attempt is in portrait mode

Portrait = open animation works / close animation works

Landscape = open animation works / close animation messed up

Portrait = open animation works / close animation works

Landscape = open animation works / close animation works


Application starts in portrait mode, then I toggle to landscape mode and then first animation attempt is performed in landscape mode

Landscape = open animation works / close animation messed up

Portrait = open animation works / close animation works

Landscape = open animation works / close animation messed up

Portrait = open animation works / close animation works


Application starts in landscape mode and first animation attempt is in landscape mode

Landscape = open animation works / close animation works

Portrait = open animation works / close animation messed up

Landscape = open animation works / close animation works

Portrait = open animation works / close animation messed up


Application starts in landscape mode, then I toggle to portrait mode and then first animation attempt is performed in portrait mode

Portrait = open animation works / close animation messed up

Landscape = open animation works / close animation works

Portrait = open animation works / close animation messed up

Landscape = open animation works / close animation works



>>Could you detail [the constraints] ?

dataView constraints are pretty basic:


Align Top to safe area = 20

Align Bottom to safe area = 20

Align Leading to safe area = 16

Align Trailing to safe area = 16


>>What is the problem you want to avoid ?

The issue is when the viewWillLayoutSubviews( ) function is been processed before the animation in the CloseView( ) function has completed. When I click the CloseButton, then the CloseView( ) function is called and the UIView_Animate( ) function (line 44 above) is called. This function should animate the dataView by moving it from "origin.x = 16 to origin.x" = "self.getScreenSize().width + 100". When the viewWillLayoutSubviews( ) function fires before the animation completes then the dataView "Align Leading to safe area = 16" constraint setting is set again, thus moving the dataView back to origin.x =16 while the animation is active, thus messing up the animation


My thoughts

I thought I would simply remove the constraints from the dataView "View" so the viewWillLayoutSubviews( ) function would no longer reposition the dataView when the animation is active since there would be no constraints to act on anymore. However, all of the other objects(views) which I have inside the dataView (that I did not mention, so I could keep my issue simplified) would all have constraint errors. As a result, I cannot remove the constraints from dataView.

Thanks, very clear.


What do you get as print ? Are they the correct value (post rotation I mean)


    @IBAction func closeView()
    {
        print("closeScoreWindow -- enter")
        print("   dataView.frame.origin.x = \(dataView.frame.origin.x)")

The major difference in animations between open and close is you refer to self.getScreenSize. Is it already updated when you call ?

Add a print to check:


     print("Before close animation screenWidth", self.getScreenSize().width)
     //move view out of the viewing area
     self.dataView.frame.origin.x = self.getScreenSize().width + 100


Could you try adding a needsLayout() at the very beginning of closeView(). But it may well be done too late, after animation is called.

If do not work, add, for testing, a 1s sleep


    @IBAction func closeView()
    {
       self.needLayout()
       sleep(1) // Just for testing purpose
        print("closeScoreWindow -- enter")
        print("   dataView.frame.origin.x = \(dataView.frame.origin.x)")

>>What do you get as print ? Are they the correct value (post rotation I mean)


Debug "print" statement output

When I switch from portrait mode into landscape mode and then click the openButton, I see the following debug:


<Note: OPEN button clicked here>

openScoreWindow -- enter

dataView.frame.origin.x = 16.0 <--- this value is expected since dataView is currently hidden, but is located at origin.x = 16

when openView( ) function is first called. In the openView( ) function, I then hardcode

"origin.x = 767" before the call to UIView.animate( ) occurs

animation complete - openScoreWindow


<Note: CLOSE button clicked here>

viewWillLayoutSubviews

dataView.frame.origin.x = 16.0

viewDidLayoutSubviews

dataView.frame.origin.x = 16.0

closeScoreWindow -- enter

dataView.frame.origin.x = 16.0 <--- this is the expected value since origin.x should be 16 when dataView is currently opened

viewWillLayoutSubviews <--- this routine is called by the system which seems to be messing up my animation request

dataView.frame.origin.x = 767.0 <--- this value was hardcoded in the closeView( ) function inside UIVIew.animate( ) since I

want my origin.x to move from 16(current) to 767 when closing the dataView, so this is good.

viewDidLayoutSubviews

dataView.frame.origin.x = 16.0 <--- this proves the "leading" constraint is reset automatically back to 16 during active animation


animation complete - closeScoreWindow . <-- this is when animation has completed



>> ... between open and close you refer to self.getScreenSize. Is it already updated when you call ?

I decided to hardcode line 17 and line 49 to have "origin.x = 767", which removes the calls to getScreenSize() completely. The issue still occurs in landscape mode.



>>Could you try adding a needsLayout() at the very beginning of closeView().

I actually tried this myself already, but just tried all of the following again just now:


- I added a call to self.view.setNeedsLayout( ) at line 5 (see below) only and the issue still occurred.

- I removed line 5 and added line 13 only and the issue still occurred.

- I added line 5 and line 13 at the same time and the issue still occurred

- Keeping line 5 and line 13, I then added calls to self.view/layoutIfNeeded( ) at line 6 and line 14 to force a call to viewWillLayoutSubviews( ) to occur. The viewWillLayoutSubviews( ) call does occur two times (as expected) but the issue still occurs


@IBAction func closeView() 
    { 
        print("closeScoreWindow -- enter") 
        print("   dataView.frame.origin.x = \(dataView.frame.origin.x)") 
        self.view.setNeedsLayout()
        self.view.layoutIfNeeded()

        //animate view moving from left to right off the screen 
        UIView.animate(withDuration: 3.0, //1 second 
            delay: 0, 
            options: .curveEaseInOut, 
            animations: { 
                self.view.setNeedsLayout()
                self.view.layoutIfNeeded()

                //move view out of the viewing area 
                self.dataView.frame.origin.x = self.getScreenSize().width + 100 
            }, 
            completion: { [weak self] finished in 
                //hide the view 
                self?.dataView.isHidden = true 
                print("   animation complete - closeScoreWindow") 
                print("") 
           }) 
    }




What I see visually when the issue occurs

When I switch from portrait mode into landscape mode and then click the openButton, I see the dataView window animate into view from the right side, moving from "origin.x = 767" to "origin.x = 16" as desired.


When I now click the closeButton, I print the origin.x value and confirm it has an expected value of 16. A call to UIView.animate( ) is then made, requesting animation of dataView to occur from the current origin.x = 16 position to a final position of origin.x = 767 and then the animation begins. However, the dataView "view" disappears from the display screen (which is wrong) and then animates back into the display area from the "left" side of the screen (which is wrong) and then stops animating when dataView reaches origin.x = 16 (which is wrong).


Visually it looks to me that the animation is moving dataView from -767 (negative) to 16 when the issue occurs, however, I cannot prove this since I never see a "print" debug statement showing me origin.x moves to a negative number. However, this is exactly what it looks like viewing the display when the issue occurs

May be a stupid proposition.


What happens if you hardcode minus 767 ?


origin.x = -767

hardcoding to -767 results:

When I switch from portrait mode into landscape mode and then click the openButton, I see the dataView window animate into view from the LEFT side now. It seems like it is moving from origin.x = -767 to origin.x = 16 as we would expect now.


When I then click the closeButton, I see the dataView window move in from the RIGHT side and stop animating when it hits origin.x = 16. It seems to be animating from positive(+) origin.x = 767 back to origin.x = 16 (ie: moving right to left). The viewWillLayoutSubviews( ) function is called (as always) right after entering the closeView( ) function occurs. This results in the -767 being switched to 16 since the dataView snaps back to it's leading constraint setting value of 16.


This handling is pretty much the opposite of what is going on when it is hardcoded to positive 767


The issue still seems to me that viewWillLayoutSubviews( ) is messing up the animation since it occurs while animation is already active


It is all very weird stuff.

We must be misunderstanding something here.


In your original post you said:

Clicking the CloseButton results in weird animation in the wrong places and the DataView does not properly go off the screen.

How does it display exactly ? What is this wrong place ?


As I cannot test your ptoject, could you try different animation for close (-100 vs +100) so that view does not completely disappear:


            animations: { 
                //move view out of the viewing area 
                self.dataView.frame.origin.x = self.getScreenSize().width - 100 
            },

The issue still seems to me that viewWillLayoutSubviews( ) is messing up the animation since it occurs while animation is already active


Maybe. But why never a problem with open ?

Did you try to delay the animation by 1 second (to leave plenty of time to complete) ; and remove the call to layout in animation closure.

>>How does it display exactly ? What is this wrong place ?


The proper desired behavior is as follows:

1) Clicking openButton should animate dataView coming into the display starting from the right side at origin.x = 767 and moving to origin.x = 16


2) Clicking closeButton should animate dataView moving out of the display, starting from current position origin.x =16 and moving out/off the right side of the screen to origin.x = 767


What is actually happening is as follows:

1) Clicking openButton animates dataView coming into the display starting from the right side at origin.x = 767 and moving to origin.x = 16

(this IS proper behavior and is good! )


2) Clicking closeButton animates dataView moving into the display, starting from origin.x = -767 and moving to origin.x =16

(this IS NOT proper behavior and is bad)



>>As I cannot test your project, could you try different animation for close (-100 vs +100) so that view does not completely disappear

hehe, I also tried this myself while trying to find an answer, but just tried it again to confirm results for you once again.


Results using 200

- dataView starts off as hidden (of course)


- I click the openButton and the openView( ) function is called which hardcodes the origin.x = 200

- openView( ) function then makes the dataVIew visible (isHidden = false) and I see the dataView display at origin.x = 200

- UIView.animate( ) is called to move the dataView from current position (origin.x = 200) to new position (origin.x = 16) to animate the view moving from right to left from 200 to 16.

- I see the dataView move right to left from 200 to 16 as desired.


- I click the closeButton and the closeView( ) function is called

- closeView( ) function prints the "origin.x" value as 16, which is correct since this is where the current dataView is positioned

- UIView.animate( ) is called to move the dataView from current position (origin.x = 16) to new hardcoded position (origin.x = 200) to animate the view moving from left to right from 16 to 200

- I see the dataVIew move left to right from -200(negative) to 16, which is not correct.


Of course viewWillLayoutSubviews( ) is called right after closeView( ) function is entered and while UIView.animate( ) call is active (ie: completion closure has not yet been called). This changes the origin.x value back to the leading constraint setting value of 16 while animation is still active. I can only assume changing the origin.x value while the animation is active, which uses the origin.x value, is somehow confusing the animation process, however, this is a guess of course as I do not understand how UIView.animate( ) behaves when the origin.x value changes while animation, which is using origin.x, is still active.

>>Maybe. But why never a problem with open ?

Exactly. This is all weird stuff. The problem is, I am not sure what other debug statements or code logic to try out to keep debugging this issue. I am at a lost of what else to try to narrow the issue down.


>>Did you try to delay the animation by 1 second (to leave plenty of time to complete)

I set the closeView( ) function animation to be 5 seconds in an attempt to really slow it down so I can look for any weird display issues, but I did not notice anything different. The dataView always comes in from the left (negative location) and stops at 16 when the issue happens.



*** I FOUND A SOLUTION THAT WORKS !!! ***


I continued to google around and found a couple of posts.


The first post indicated that if you have a constraint assigned to a view, that you want to animate, then you should be animating the view by changing the constraint values instead of the origin.x or origin.y values.


As a result I decided the create an IBOutlet for the leading constraint to the dataView (ie: viewLeadingAlignment_Constraint) and set it accordingly inside the openView and closeView functions. I guess since I am now using a "constraint value" for animation, then when the viewWillLayoutSubview( ) call occurs, while animation is active, then the view still maintains it's current constraint value location and no longer gets reset back to the 16 constraint I had before.


Anyway, I ran the program and the dataView appears and disappears without any animation at all, so this did NOT work.

However, please read more below ...


The second post indicated the following "Two important notes":


  1. You need to call
    layoutIfNeeded
    within the animation block. Apple actually recommends you call it once before the animation block to ensure that all pending layout operations have been completed
  2. You need to call it specifically on the parent view (e.g.
    self.view
    ), not the child view that has the constraints attached to it. Doing so will update all constrained views, including animating other views that might be constrained to the view that you changed the constraint of (e.g. View B is attached to the bottom of View A and you just changed View A's top offset and you want View B to animate with it)


Based on these two notes, I added calls to self.view.layoutIfNeeded( ) after each line where I set the IBOutlet constraint value inside the openView and closeView functions and then everything worked as desired (see updated/working code below). I can now open/close the dataView in portrait and landscape modes with expected animation.



    override func viewDidLoad()
    {
       dataView.isHidden = true
    }

    @IBAction func openView()
    {
        //ensure view is out of the viewing area
        viewLeadingAlignment_Constraint.constant = getScreenSize().width + 100
        self.view.layoutIfNeeded()

        //make view visible for eventual animation
        dataView.isHidden = false

        //animate view appearing
        UIView.animate(withDuration: 1.0,
            delay: 0,
            options: .curveEaseInOut,
            animations:
            {
                self.viewLeadingAlignment_Constraint.constant = 16
                self.view.layoutIfNeeded()
            },
            completion: nil )
      }


    @IBAction func closeView()
    {
        //animate view disappearing
        UIView.animate(withDuration: 1.0,
            delay: 0,
            options: .curveEaseInOut,
            animations: {
                //move view out of the viewing area
                self.viewLeadingAlignment_Constraint.constant = self.getScreenSize().width + 100
                self.view.layoutIfNeeded()
            },
            completion: { [weak self] finished in
                //hide view
                self?.dataView.isHidden = true
            })
    }



Based on the second posting #2 statement, perhaps my original design of moving dataView by changing the origin.x value was not working properly since the dataView "view" included other views (buttons, labels, etc..) inside of it. I was requesting animation to occur on dataView only, so the other views, which are located inside of dataView, were not part of this animation request and thus bad/weird things happened during animation, however, this is just a complete guess.

The problem was that constraints were conflicting with the new frame setting.

I assume you mean the issue is that animation must use constaints to ensure all subviews are animated as well. Since I was simply changing the origin.x value for dataView (and not the constraint value) then the animation logic could not figure out how to move (animate) the dataView properly since the new origin.x value and the existing fixed constraint vlaue were conflicting.


Anyway, I thought constraint settings were only applied when the orientation changed and this was a wrong assumption of mine.


Thanks for all your help Claude !

What I mean is that the set of constraints were not consistent with the new frame origin.


Great it works for you now.


Don't forget to close this (long) thread.

Now that I am using the leading constraint value for positioning of the dataView (instead of setting origin.x) then the animation works properly. However, I just realized that setting the constraint off the screen (line 9) before animation starts, results in an "Unable to simultaneously satisfy constraints" warning being generated in the debugger console area. I receive the same warning again when line 36 occurs when closing the dataView as well.


Since I am resetting the constraint to be off the screen (line 9), in preparation for the "open" animation, I am assuming the trailing constraint no longer can be satisifed so this is what is causing the warning. I am not sure how to resolve this warning though. If I disable the trailing constraint then I think all of constraints for all the views (buttons, labels, etc..) which are inside dataView "view" will now have constraint issues since they all depend on dataView constraints being correct (blue lines).


Any idea how to address this debugger constraint warning?