UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 34 -- why the flags rotate twice -- figured out

Forums > 100 Days of SwiftUI

I originally tried this:

      ForEach(0..<3) { number in
          Button {
              flagTapped(number)
          } label: {
              Image(countries[number])
              }
              .clipShape(RoundedRectangle(cornerRadius: 20))
              .shadow(radius: 10, x:5, y:5)
              .rotation3DEffect(.degrees(360), axis: (x: 0, y: (selectedFlag == number ? 1 : 0), z: 0))
              .animation(.default, value: selectedFlag)
  }

That doesn't work. I am not sure why.

I got Paul's solution to work, but the flag rotates BACKWARDS when the alert is dismissed. I was puzzled by that, but eventually realized that SwiftUI does not know that .degrees(360) == degrees(0)

1      

Hey Now!

You're not sure why your original code doesn't work?
Let's find out! Here's one solution, with some detail.

View Factory


Think of ForEach as a view factory. It's job is to create a separate View for each thing in the parenthesis.

// This >view factory< creates a view for the integers 0, 1, and 2
ForEach(0..<3) { number in
          // ... snip ...
  }

In your code, it will create a view for each integer, 0, 1 and 2. What view will it create?

Buttons


Your view factory will create three Button objects. Each button object has an image and a function to execute, if pushed.

// This is what the view factory will make.
    Button {
        flagTapped(number) // This is the function your button executes when tapped.
    } label: {
         Image(countries[number])  // This image comes from your assets folder in your application.
    }

Button Decoration


You've added two decorators to make your buttons look nice! For the most part, we can ignore these to focus on your animation question problem.

    Button {
        flagTapped(number) // This is the function your button executes when tapped.
    } label: {
        Image(countries[number])  // This image comes from your assets folder in your application.
    }
    .clipShape(RoundedRectangle(cornerRadius: 20))
    .shadow(radius: 10, x:5, y:5)

Planning Animation


Grab a pencil and a sheet of paper. Draw three flags in a column: Germany, Ireland, and France.
Above this column write "Before".

Right next to the colum of flags, draw another column labeled "After" and draw the same three flags.

Let's assume that the third flag, France, is the correct answer. So your correctAnswer variable is 2.

Swift will update any view whenever one of its @State variables changes. Because you're specifying an animation when redrawing a view, Swift handles drawing the hundreds of variations between your starting and ending views.

Consequently, we do NOT want to change any @State vars for the two incorrect flags. We'll need an @State variable for the flag that changes when the player taps on the correct flag. Note! It should only change for the correct flag and not the other two.

So on your sheet of paper, write down a variable named rotationAmount next to each flag in column 1. Add the variable rotationAmount next to each flag in column 2. Because you don't want the two incorrect flags to rotate, write the integer 0 in both column 1 and column 2 for the incorrect flags.

Write the number 0 in column 1 for France's flag, but change it to 360 for column 2.

Views are redrawn when their @State vars change. Consequently, the incorrect flags won't redraw themselves, since their rotationAmount vars do not change.

How to signal a change?


Now the question becomes, How do you change the rotationAmount var for France's flag? This is the job for the flagTapped()function.

Notice when each Button is made, SwiftUI picks a rotationAmount based on whether it's the correct answer. It's either rotationAmount (correctAnswer) or 0.0 (incorrect). Also notice rotationAmount is 0.0 until the flag is tapped! If the correct flag is tapped, its rotationAmount changes to 360. This triggers the view to redraw itself. Redrawing itself triggers SwiftUi to change the view from column 1 to column 2 using the animation you specified.

The other two flags are NOT redrawn because their @State vars did not change.

    Button {
        flagTapped(number) // This is the function your button executes when tapped.
    } label: {
         Image(countries[number])  // This image comes from your assets folder in your application.
    }
    // When each button is made, SwiftUI picks the correct rotationAmount.
    .rotation3DEffect( .degrees( number == correctAnswer ? rotationAmount: 0.0), axis: (x:0, y:1, z:0))

Debugging Hint


To see the animation in slow motion in the simulator, tap the option Slow Animations in the simulator's Debug menu! Now you can see the flag rotate 360 degrees. For fun, change the rotationAmount to 200, or 270 to view the difference.

Original Code

ForEach(0..<3) { number in
          Button { flagTapped(number) } 
              label: { Image(countries[number]) }
              .rotation3DEffect(.degrees(360), axis: (x: 0, y: (selectedFlag == number ? 1 : 0), z: 0))
              .animation(.default, value: selectedFlag)
  }

3      

I am guessing that having the .degrees(360) both before and after selectedFlag changes causes SwiftUI to decide there's nothing to animate?

1      

@Bnerd  

How did you solve the .opacity one? I ended up adding an additional @State variable that

FlagImage(name: countries[number])
                            .opacity(Double(selectedFlag == number ? 1 : opacityV))

whereas opacityV starts as 1.0, but once you press the button changes to 0.5 and with the reset button goes back to 1.0 This works but I am curious if there is a simpler solution...

1      

I'm just going to paste the code here:

.opacity(((selectedFlag == number) || (selectedFlag == -1))  ? 1 : 0.3)

1      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.