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

I did not look in details.


But a frequent solution is to lower the priority of one offending constraints (the one that is declared relaxed in log) to 999 instead of 1000.

That worked. Thanks for the tip.

Last point.


You mention in the post marked as solution 2 posts where yopu found information.


It is often helping to attach the post URL in the message. There maybe additional useful information there, and they also merit credit for having in fact given the solution.