in SwiftUI, in a ForEach(0 ..< 3), animate the tapped button only (not all 3), animate the other 2 differently

Viewed 677

After a full day trying to animate these buttons I give up and ask for help. I would like to animate the correct button only like this: .rotation3DEffect(.degrees(self.animationAmount), axis: (x: 0, y: 1, z: 0)) and at the same time make the other two buttons fade out to 25% opacity.

When the player clicks the wrong button I would like to animate the wrong button like this: .rotation3DEffect(.degrees(self.animationAmount), axis: (x: 1, y: 1, z: 1)) (or anyway else you can think to indicate disaster) and leave the other two alone.

After that happened I would like the alert to show.

Below is my code. I commented what I would like to do and where if at all possible. It all works like I want but cannot get the animation going.

Thanks for your help in advance

    import SwiftUI

struct ContentView: View {
   @State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"]
        @State private var correctAnswer = Int.random(in: 0...2)
        @State private var showingScore = false
        @State private var scoreTitle = ""
        @State private var userScore = 0
        @State private var userTapped = ""
         @State private var animationAmount =  0.0
    
        var body: some View {
            ZStack {
                LinearGradient(gradient: Gradient(colors: [.blue, .black]), startPoint: .top, endPoint: .bottom)
                    .edgesIgnoringSafeArea(.all)
                
                VStack(spacing: 20) {
                    VStack {
                        Text("Tap the flag of...")
                            .foregroundColor(.white).font(.title)
                        Text(countries[correctAnswer])
                            .foregroundColor(.white)
                            .font(.largeTitle).fontWeight(.black)
                    }
                    
                    ForEach(0 ..< 3) { number in
                        Button(action: {
                                self.flagTapped(number)
                            if self.correctAnswer == number {
                                //animate the correct button only like this:
                                //.rotation3DEffect(.degrees(self.animationAmount), axis: (x: 0, y: 1, z: 0))
                                // and
                                // make the other two buttons fade out to 25% opacity
                            } else {
                                // animate the wrong button like this:
                                //.rotation3DEffect(.degrees(self.animationAmount), axis: (x: 1, y: 1, z: 1))
                            }
                        }) {
                            Image(self.countries[number])
                                .renderingMode(.original)
                                .clipShape(Capsule())
                                .overlay(Capsule().stroke(Color .black, lineWidth: 1))
                                .shadow(color: .black, radius: 2)
                        }
                    }
                     
                    Text ("your score is:\n \(userScore)").foregroundColor(.white).font(.title).multilineTextAlignment(.center)
                }
                
            }
            .alert(isPresented: $showingScore) {
                Alert(title: Text(scoreTitle), message: Text("You chose the flag of \(userTapped)\nYour score is now: \(userScore)"), dismissButton: .default(Text("Continue")) {
                    self.askQuestion()
                    })
            }
        }
        func flagTapped(_ number: Int) {
            userTapped = countries[number]
            if number == correctAnswer {
                scoreTitle = "Correct"
                userScore += 1
            } else {
                scoreTitle = "Wrong"
                userScore -= 1
            }
            showingScore = true
        }
        
        func askQuestion() {
            countries.shuffle()
            correctAnswer = Int.random(in: 0...2)
        }
    
    }

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
2 Answers

I had a problem similar to yours when solving this challenge. I figured out a solution without using DispatchQueue.main.asyncAfter. I made the following as the final solution:

  1. Spin around 360 degrees the correct chosen flag and fade out the other flags to 25 % of opacity.
  2. Blur the background of the wrong flag chosen with red and fade out the other flags to 25 % of opacity.

Here is the full solution (I comment on the parts that were important to achieve the above solution):

import SwiftUI

// Create a custom view
struct FlagImage: View {
    var countryFlags: String
    
    var body: some View {
        Image(countryFlags)
            .renderingMode(.original)
            .clipShape(Capsule())
            .overlay(Capsule().stroke(Color.black, lineWidth: 1))
            .shadow(color: .black, radius: 2)
    }
}

struct ContentView: View {
    
    @State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"].shuffled()
    
    @State private var correctAnswer = Int.random(in: 0...3)
    @State private var showingScore = false
    @State private var scoreTitle = ""
    
    @State private var userScore = 0
    
    // Properties for animating the chosen flag
    @State private var animateCorrect = 0.0
    @State private var animateOpacity = 1.0
    @State private var besidesTheCorrect = false
    @State private var besidesTheWrong = false
    @State private var selectedFlag = 0
    
    var body: some View {
        
        ZStack {
            LinearGradient(gradient: Gradient(colors: [.blue, .black]), startPoint: .top, endPoint: .bottom).edgesIgnoringSafeArea(.all)
            
            VStack(spacing: 30) {
                
                VStack {
                    Text("Tap on the flag!")
                        .foregroundColor(.white)
                        .font(.title)
                    
                    Text(countries[correctAnswer])
                        .foregroundColor(.white)
                        .font(.largeTitle)
                        .fontWeight(.black)
                }
                
                
                ForEach(0 ..< 4) { number in
                    Button(action: {
                        
                        self.selectedFlag = number
                        
                        self.flagTapped(number)
                        
                    }) {
                        
                        FlagImage(countryFlags: self.countries[number])
                    }
                    // Animate the flag when the user tap the correct one:
                    // Rotate the correct flag
                    .rotation3DEffect(.degrees(number == self.correctAnswer ? self.animateCorrect : 0), axis: (x: 0, y: 1, z: 0))
                    // Reduce opacity of the other flags to 25%
                    .opacity(number != self.correctAnswer && self.besidesTheCorrect ? self.animateOpacity : 1)
                    
                    // Animate the flag when the user tap the wrong one:
                    // Create a red background to the wrong flag
                    .background(self.besidesTheWrong && self.selectedFlag == number ? Capsule(style: .circular).fill(Color.red).blur(radius: 30) : Capsule(style: .circular).fill(Color.clear).blur(radius: 0))
                    // Reduce opacity of the other flags to 25% (including the correct one)
                    .opacity(self.besidesTheWrong && self.selectedFlag != number ? self.animateOpacity : 1)
                    
                }
                Spacer()
                
                Text("Your total score is: \(userScore)")
                    .foregroundColor(Color.white)
                    .font(.title)
                    .fontWeight(.black)
                
                Spacer()
                
            }
        }
        .alert(isPresented: $showingScore) {
            Alert(title: Text(scoreTitle), dismissButton: .default(Text("Continue")) {
                self.askQuestion()
                })
        }
        
    }
    
    func flagTapped(_ number: Int) {
        
        if number == correctAnswer {
            scoreTitle = "Correct!"
            
            userScore += 1
            
            // Create animation for the correct answer
            withAnimation {
                self.animateCorrect += 360
                self.animateOpacity = 0.25
                self.besidesTheCorrect = true
            }
        } else {
            scoreTitle = "Wrong!"
            
            // Create animation for the wrong answer
            withAnimation {
                self.animateOpacity = 0.25
                self.besidesTheWrong = true
            }
        }
        showingScore = true
    }

    func askQuestion() {
        // Return the booleans to false
        besidesTheCorrect = false
        besidesTheWrong = false
        countries = countries.shuffled()
        correctAnswer = Int.random(in: 0...3)
    }
    
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Basically, my solution is to add ternary operators inside the view modifiers (e.g., .rotation3DEffect(...), .opacity(...) and .background(...)) after the Button view. The tricky part is to correctly combine the checking condition.

I prefer to add the withAnimation modifier to my flagTapped function. In this place I have more control of the animations if the user select a correct or a wrong flag.

I made a small change to the original challenge: just add one more flag to the view.

The final result when a user presses the correct and wrong flag is here:

Screenshot.

I had the same problem. Here is my solution (only the code that's different from yours):

The ForEach:

ForEach(0 ..< 3, id: \.self){ number in
                Button(action: {
                    withAnimation{
                        self.tappedFlag = number
                        self.flagTapped(number)
                    }
                }){
                    FlagImage(imageName: self.countries[number])
                }
                .rotation3DEffect(.degrees(self.isCorrect && self.selcectedNumber == number ? 360 : 0), axis: (x: 0, y: 1, z: 0))
                .opacity(self.isFadeOutOpacity && self.selcectedNumber != number ? 0.25 : 1)
                    
                .rotation3DEffect(.degrees(self.wrongAnswer && number != self.correctAnswer ? 180 : 0), axis: (x: 1, y: 0, z: 0))
                .opacity(self.wrongAnswer && number != self.correctAnswer ? 0.25 : 1)
                
            }

The alert:

.alert(isPresented: $showingScore){
        if scoreTitle == "Correct"{
            return Alert(title: Text(scoreTitle), message: Text("Your score is \(userScore)"), dismissButton: .default(Text("Continue")){
                    self.askQuestion()
                })
        }else{
            return Alert(title: Text(scoreTitle), message: Text("That is the flag of \(countries[tappedFlag]), you lost one point!"), dismissButton: .default(Text("Continue")){
                self.askQuestion()
            })
        }
    }

The two functions:

func flagTapped(_ number: Int){
    self.selcectedNumber = number
    self.alreadyTapped = true
    if number == correctAnswer{
        scoreTitle = "Correct"
        userScore += 1
        self.isCorrect = true
        self.isFadeOutOpacity = true
    }else{
        self.wrongAnswer = true
        scoreTitle = "Wrong"
        if userScore != 0{
            userScore -= 1
        }
    }
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.showingScore = true
    }
}

func askQuestion(){
    countries = countries.shuffled()
    correctAnswer = Int.random(in: 0...2)
    self.isCorrect = false
    self.isFadeOutOpacity = false
    self.wrongAnswer = false
}

You have to declare some new variables :)

I hope I could help.

PS: There is a playlist on Youtube for 100DaysOfSwiftUI with the solutions to almost every own task.

https://www.youtube.com/watch?v=9AUGceRIUSA&list=PL3pUvT0fmHNhb3qcpvuym6KeM12eQK3T1

Related