Hallo fellow ranchers!
I am sitting on the porch of my little swift code farm in Germany and noticed, that no body has posted a solution to use a StackView in the Quiz app and still animate it. I have tackled the problem and thought I share my experience here and my solution as well. I have to admit, that the end result is a bit different Animation then we had before, but I think it delivers the same impression.
Now I want to share my little journey, that brought me to my conclusion or idea:
tl/dr: Spoilers ahead! StackView manages a value .isHidden for each View it well stacks. This value can be changed during runtime, and this change can be animated. Also the StackView has a property called arrangedSubviews which is an array of all the views the stack displays. So the way I chose to go was to hide the currentQuestionLabel, unhide the nextQuestionLabel, and then put the currentQuestionLabel at the 0 Position in the Array so I could repeat the identical process again
First I put the current- and nextQuestionLabel in a horizontal stack, and than this new questionStack in a vertical StackView together with the answerLabel and the showAnswerButton.
As I now tried to animate the Center X Constraints of the two labels as before, I was startled. Both labels were gone! What~@!? Even in the Debug View Hierarchy it was hard to make them out or what had happened to them. I think somehow their width went to zero, or something…
"Well, lets apply some constraints to thees bloody labels then!“ someone might say. But the text of the labels varies, so the constraint would have to be very dynamic as well. Which means one would have to calculate it and maybe store it and I thought there simply must be a more easy maybe even an elegant solution.
So I started my research into StackViews
As a starting point I used the idea, that a StackView can hide his Subviews, which was nicely hinted at in the chapter itself. Thanks! After a little reading I already had learned, that this subview hiding would be animated. All managed by the bloody StackView itself. “Wonderful! I am obviously on the right track!” I exclaimed in joy.
After implementing this I had already an animated changing of the asked question. First I would hide the current question and unhide the next one. That would result in the currentQuestionLabel swishing out to the right side of the screen into the abyss and the nextQuestionLabel appearing from behind where the other would sit just a second ago. After that I would turn it around. Hide the nextQuestionLable and unhide the other one. Now the nextQuestionLabel would move swiftly to the right side of the screen and beyond, where no questionLabel had gone before and the currentQuestionLabel would appear again from behind.
This was nice but not as nice as the way we had it before. Since it also conways a different meaning. Right!? It feels more like there are two parties taking turns in asking you simple questions. Questions one could easily look up, or just answer them them self… why is my phone asking me!? Normally I ask it! Not vice versa! What has happened to Siri? Why? Is he okay?? (yes my Siri is a man, a MAN!, as should be yours by the way!
Back to the story! I had an Idea. While I typed the commands to hide the view I noticed that it uses subscript syntax, on a property called arrangedSubviews. Well, Well… could that be a list… of all the subviews that the stack a r a n g e s?
Well off course! Why would you ask such a obvious question? After reading up on this I also learned, that the StackView indeed keeps an array of all the views it displays and manages. I wondered if it would be possible to simply change the order of views in this array, so that the question that is hidding always moves out to the right side.
As you might guess this assumption was indeed right. After some testing I found the shortest route to achieve my goal. And this is the point where I want to stop this dissipated exposition and simply show you my actual code.
After that I have some final thoughts and remaining Questions.
class ViewController: UIViewController {
@IBOutlet var currentQuestionLabel: UILabel!
@IBOutlet var nextQuestionLabel: UILabel!
@IBOutlet var answerLabel: UILabel!
@IBOutlet var questionsStackView: UIStackView!
let questions: [String] = ["From what is cognac made?",
"What is 7+7?",
"What is the capital of Vermont?",
"What are the first four numbers of Pi?",
"How is the weather?"]
let answers: [String] = ["Grapes",
"14",
"Montepelier",
"3,141",
"It's great, of course!"]
var currentQuestionIndex: Int = 0
// Declaring two slots, for both question UIViews
var currentQuestionInStack : UIView!
var nextQuestionInStack: UIView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// set the label's initial alpha
nextQuestionLabel.alpha = 0
}
override func viewDidLoad() {
super.viewDidLoad()
currentQuestionLabel.text = questions[currentQuestionIndex]
}
func animateLabelTransitions() {
// Force any outstanding layout changes to occur
view.layoutIfNeeded()
UIView.animate(withDuration: 1,
delay: 0,
usingSpringWithDamping: CGFloat(0.9),
initialSpringVelocity: CGFloat(0),
options: [.curveLinear],
animations: {
// change the alpha of the two Labels
self.currentQuestionLabel.alpha = 0
self.nextQuestionLabel.alpha = 1
// Because the Stack View is hiding the Views and not the Labels we will use the two slots we declared earlier.
// We tell the StackView to hide and unhide each View, and the StackView takes care of the animation.
self.currentQuestionInStack = self.questionsStackView.arrangedSubviews[1]
self.nextQuestionInStack = self.questionsStackView.arrangedSubviews[0]
self.nextQuestionInStack.isHidden = false
self.currentQuestionInStack.isHidden = true
self.view.layoutIfNeeded()
},
completion: { _ in
// Change the reference to the next- and current question label **and View**
swap(&self.currentQuestionLabel, &self.nextQuestionLabel)
swap(&self.currentQuestionInStack, &self.nextQuestionInStack)**
// Put the nextQuestionLable at the first position in the questionsStackView array
self.questionsStackView.insertArrangedSubview(self.nextQuestionInStack, at: 0)
})
}
@IBAction func showNextQuestion(_ sender: AnyObject) {
currentQuestionIndex += 1
if currentQuestionIndex == questions.count {
currentQuestionIndex = 0
}
let question: String = questions[currentQuestionIndex]
nextQuestionLabel.text = question
answerLabel.text = "???"
animateLabelTransitions()
}
@IBAction func showAnswer(_ sender: AnyObject) {
let answer: String = answers[currentQuestionIndex]
answerLabel.text = answer
}
}
So You will see, that the animation is different, because the next question is not coming in from the left, like before, but more from behind, like some gangster with a knife. Still I think the “feel” of it is quiet similar.
Regarding my first approach I am still not sure what happened to the labels, when I tried to animate their center X constraint. If anyone knows, I would be happy to hear that.
I also wonder about my usage of the .insertArrangedSubview() method. At first I used it in cooperation with the .removeArrangedSubview() method. But in practice I could not see any difference so I simply got rid of it. Am I right? No honestly! Is there something I missed? What do you think?
A last thought: I use the currentQuestionInStack and nextQuestionInStack variables to simply store a reference to the view, so I can easily switch them after the animation is done and simply reuse my code for the next transition. Do you think there is a more elegant way to do this?
Okay. Now I am really done. I will now go on do the world Trotter challenge. Probably have to reprogram that whole thing, because it was so long ago and I can not remember what I did there. We’ll see.
Best Wisches from Germany
BrutusD