Silver using UILayoutGuide


#1

I removed the alpha stuff because it was getting in the way. (but you could swap the alphas of the labels in the completionHandler
of animateWithDuration)

Changes in properties.
The property observers are used to remove the current constraint from the view, and then add the new constraint whenever I set them.

@IBOutlet weak var currentQuestionLabelCenterXContraint: NSLayoutConstraint! {
        didSet {
            if let oldConstraint = oldValue {
                oldConstraint.active = false
            }
            currentQuestionLabelCenterXContraint.active = true
        }
}

@IBOutlet weak var nextQuestionLabelCenterXConstraint: NSLayoutConstraint! {
        didSet {
            if let oldConstraint = oldValue {
                oldConstraint.active = false
            }
            nextQuestionLabelCenterXConstraint.active = true
        }
}

New Properties. The layout guide and a constraint that change the position of the guide in relation to the view.

let layoutGuide = UILayoutGuide()

var guideConstraint = NSLayoutConstraint() {
        didSet {
                oldValue.active = false
                guideConstraint.active = true
        }
}

In viewDidLoad() I added these lines:

// Add the guide to the view. (Comes from the example in the documentation of UILayoutGuide) 
view.addLayoutGuide(layoutGuide)
// Make the guide’s width always the width of the view
layoutGuide.widthAnchor.constraintEqualToAnchor(view.widthAnchor).active = true
// Constraints (Guide - Labels). In cases like these is that the property observers are handy.
currentQuestionLabelCenterXContraint = layoutGuide.trailingAnchor.constraintEqualToAnchor(currentQuestionLabel.centerXAnchor)
nextQuestionLabelCenterXConstraint = layoutGuide.leadingAnchor.constraintEqualToAnchor(nextQuestionLabel.centerXAnchor)

// Here is the new updateOffScreenLabel()

func updateOffScreenLabel() {
        guideConstraint = layoutGuide.centerXAnchor.constraintEqualToAnchor(view.leadingAnchor)
}

And finally the animateLabelTransitions()
Changing the guide constraint, and swapping the labels text.

func animateLabelTransitions() {
        
        // Force any outstanding layout changes to occur
        view.layoutIfNeeded()
        
       // Removing the old, and adding the new constraint to the view.
        guideConstraint = layoutGuide.centerXAnchor.constraintEqualToAnchor(view.trailingAnchor)
        
        UIView.animateWithDuration(0.5, delay: 0, options: [], animations: {
            self.view.layoutIfNeeded()
            }, completion: { _ in
                swap(&self.nextQuestionLabel.text, &self.currentQuestionLabel.text)
                self.updateOffScreenLabel()
        })
}

#2

Thanks for sharing this solution @gotfried gotfried.

If I do understand the value of using LayoutGuide, I’m a bit skeptical about it being actually needed in this particular challenge - as per its complexity vs. using the view frame width. **That being said, it’s important for us to learn about LayoutGuide which will come handy in other situations, I’m not saying anything against it!

While testing the rotation of the simulator, I could notice that the view frame was automatically being updated, switching from 375.0 to 667.0 (iPhone 6S), which therefore allows the screenWidth constant to always be large enough for the nextQuestion to be offscreen.

This might have been an Apple update post publication of the Big Nerd Ranch iOS Programming guide, but it would be good to get some confirmation from the experts :slight_smile:


#3

PS: if you want to keep the alpha updates, simply include it as part of your updateOffScreenLabel method.

func updateOffScreenLabel(){
   layoutGuideConstraint = layoutGuide.centerXAnchor.constraintEqualToAnchor(view.leadingAnchor)
   currentQuestionLabel.alpha = 1
   nextQuestionLabel.alpha = 0
}

#4

+1 to the frame width always being updated.
+1 to still being a good idea to learn about it

Looks like I’ll be downloading the sample solutions to see the nerdlution.


#5

After the rotation view.frame.width will indeed give the correct value but the problem they are referring to was that updateOffscreenLabel() was setting the constraint constant to a FIXED width… to see this:

  1. Remove all the transparency stuff (so you can actually see next question)
  2. Click Next Question
  3. NOW rotate to landscape

You should see the next question 25% on the screen or so because it’s only negatively offset by the portrait length (when it should now be offset by the landscape length).

I found an in-between solution that seems to be pretty simple. First, I’m using a UILayoutGuide as the problem suggested:

var distanceBetween = UILayoutGuide()

//inside viewDidLoad()
view.addLayoutGuide(distanceBetween)
distanceBetween.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1)
  .isActive = true
distanceBetween.trailingAnchor.constraint(equalTo: currentQuestionLabel.centerXAnchor)
  .isActive = true
distanceBetween.leadingAnchor.constraint(equalTo: nextQuestionLabel.centerXAnchor)
  .isActive = true

Then at the beginning of animateTransitions():

self.currentQuestionLabelXConstraint.constant += view.frame.width

And in the animation completion handler:

self.currentQuestionLabelXConstraint.constant = 0

So I’m still using the frame width but only at the exact moment the animation is happening, when it’s guaranteed to be accurate - unless there is some race condition with rotating the screen in the middle of an animation.

I’m not sure this is better or worse but it does avoid tearing down and building new constraints every time.

Actually I should test both approaches in slow mode.


#6

Both approaches have flaws if you rotate during the animation. Either way obviously some of the constants are getting set at the beginning of the animation and then ignored when they change in the middle.

My approach looks weird going from narrow to wide (since the position is going to be off until it snaps into place after the animation ends)… and the previous posters behaves weird going from wide to narrow… the current label will first animate far to the LEFT! and then back to the right and offscreen. I presume that’s because core animation is using the initial width of the landscape view somewhere in it’s calculations.

Aside:

I’m not sure I understand why we’re changing things in the top-level of animateLabelTransitions though. This seems to work just as well and express the intent more clearly:

animations:{ () -> Void in
  self.guideConstraint = self.distanceBetween.centerXAnchor.constraint(equalTo: self.view.trailingAnchor)
  self.view.layoutIfNeeded()
},

Am I missing something?