WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Project 2 Challenge 3

Forums > 100 Days of SwiftUI

After finishing all 8 rounds when I press on the correct answer it doesn't show the alert. And when I press on something else it immediately shows the Finished alert even though I'm resetting the numberOfRounds variable

struct ContentView: View {

    @State private var showALert = false
    @State private var isFinished = false
    @State private var numberOfRounds = 1
    @State private var scoreTitle = ""

    @State private var correctAnswer = Int.random(in: 0...2)
    @State private var score = 0

    @State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"].shuffled()

    var body: some View {
        ZStack {
            RadialGradient(stops: [
                .init(color: Color(red: 0.1, green: 0.2, blue: 0.45), location: 0.3),
                .init(color: Color(red: 0.76, green: 0.15, blue: 0.26), location: 0.3)
            ], center: .top, startRadius: 200, endRadius: 700)
            .ignoresSafeArea()

            VStack {
                Spacer()

                Text("Guess the Flag")
                    .font(.title.bold())
                    .foregroundColor(.white)

                VStack(spacing: 15) {
                    VStack {
                        Text("Tap the flag of")
                            .font(.subheadline.weight(.heavy))
                            .foregroundStyle(.secondary)

                        Text(countries[correctAnswer])
                            .font(.largeTitle.weight(.semibold))
                    }

                    ForEach(0..<3) { number in
                        Button {
                            flagTapped(number)
                        } label: {
                            Image(countries[number])
                                .renderingMode(.original)
                                .clipShape(Capsule())
                                .shadow(radius: 5)
                        }
                    }
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 20)
                .background(.regularMaterial)
                .clipShape(RoundedRectangle(cornerRadius: 20))

                Spacer()
                Spacer()

                Text("Score: \(score)")
                    .foregroundColor(.white)
                    .font(.title.bold())

                Text("Round: \(numberOfRounds)")
                    .foregroundColor(.white)
                    .font(.title2)

                Spacer()
            }
            .padding()
        }
        .alert(scoreTitle, isPresented: $showALert) {
            Button("Continue", action: askQuestion)
        } message: {
            Text("Your score is \(score)")
        }

        .alert("Finished", isPresented: $isFinished) {
            Button("Restart", action: restart)
        } message: {
            Text("Your score is: \(score)")
        }
    }

    func flagTapped(_ number: Int) {
        if numberOfRounds >= 8 {
            restart()
            } else {
            if number == correctAnswer {
                scoreTitle = "Correct"
                score += 1
            } else {
                scoreTitle = "Wrong"
            }
        }

        numberOfRounds += 1
        showALert = true
    }

    func askQuestion() {
        countries.shuffle()
        correctAnswer = Int.random(in: 0...2)
    }

    func restart() {
        isFinished = true
        score = 0
        numberOfRounds = 1
    }
}

   

Every time you see a view on screen, it's representing a STATE of your game. You think your game is in ONE state and should have a particular look, or alert showing. But you are confused when you see alert boxes appear when you're not expecting them.

Blinking Lights


One technique you should add to your toolkit is to add temporary Text() views to your views detailing your view's current state. In ancient times, computers had blinking lights to show operators the state of certain registers, and accumulators.

Example:

VStack {
        Spacer()
        // ADD BLINKING LIGHTS HERE -------------------------
        Text("Playing round: \(numberOfRounds)")
        Text("Correct Flag: \(correctAnswer)" )
        Text("showAlert:  \(showAlert ? "True" : "False") " )
        Text("isFinished: \(showAlert ? "True" : "False") " )
        Text("scoreTitle: \(scoreTitle)" ) 

        Text("Guess the Flag").font(.title.bold())

As you play your game, ask yourself:
"When I am on round 6, and showAlert is true, is this what the view should look like?"
"When I am on round 4, and press the wrong flag, what is the state of my view? Is this the view I am expecting?"
"When I finish a game, how are my @State variables set? Why do I see unexpected results?"

Note

You could use either syntax:

 Text("showAlert: \(showAlert.description)  " )  //  <--- Perfectly cromulent
 Text("showAlert: \(showAlert ? "True" : "False") " )

Either is fine. I prefer the second because I can modify the text to help frame the event and describe my intentions.

 Text("showAlert: \(showAlert.description)  " )  //  <--- maybe unhelpful?
 Text("showAlert: \(showAlert ? "True: NEED TO SHOW SCORE" : "False") " )

   

I guess we could just tell you the answer. (Someone probably will.)

But at this point in the 100 Days story, I'd recommend you learn some techniques that will help you evaluate and debug your code. In some respects, debugging your logic might be 70% of learning SwiftUI programming!

Take a crack at solving this using the blinking light technique.

Please report back what you find!

   

@Obelix

Thanks for your help. After studying my code and following your tips, I realised that setting isFinished=true inside the restart() is the one causing the problem. It keeps prompting the alert message to appear. So, removed it from inside the message and added it inside the flagTapped() function instead of calling the restart() directly.

   

Nice!

It's still a useful technique to add blinking lights to your views. They'll give you a great idea when each @State variable changes. Plus, you can look at the different @State variables and make a determination of how you want the view to appear.

Red fields? Animated flags? New buttons?

You control how the views appear based on @State variables. Adding the blinking lights can help you make those choices.

Please do us a favor and mark your answer as "Solved", seeing as how you found your logic error.

   

Here a possible way to keep thing tidy and easier to manage

Create a file called AlertItem, and put this in

struct AlertItem {
    var title: String
    var message: Text
    let button: String
}

Then you can add the alerts required EG.

struct AlertItem {
    var title: String
    var message: Text
    let button: String

// Must have static to access it 
    static func flagCorrect(score: Int = 0) -> AlertItem {
        let alert = AlertItem(
            title: "Correct",
            message: Text("Your score is \(score)"),
            button: "Continue"
        )
        return alert
    }

    static func flagWrong(score: Int = 0) -> AlertItem {
        let alert = AlertItem(
            title: "Wrong",
            message: Text("Your score is \(score)"),
            button: "Continue"
        )
        return alert
    }

    static func finishedGame(score: Int = 0) -> AlertItem {
        let alert = AlertItem(
            title: "Game Over",
            message: Text("Your final score is \(score)"),
            button: "OK"
        )
        return alert
    }

    // No Parameter to pass in Alert
    static var generalAlert: AlertItem {
        AlertItem(
            title: "General Alert",
            message: Text("This is a general alert!"),
            button: "OK")
    }
}

Then in your ViewModel add your properities and methods

extension ContentView {
    class ViewModel: ObservableObject {

        // MARK: - Alert Properties
        @Published var showingAlert = false
        @Published var alertTitle = ""
        @Published var alertMessage: Text?
        @Published var alertButton = ""

        @Published var score = 0
        @Published var answer = Bool.random() // <- This is only to give different results

        func flagTapped() {
            if answer {
                score += 1
                alertTitle = AlertItem.flagCorrect().title
                alertMessage = AlertItem.flagCorrect(score: score).message
                alertButton = AlertItem.flagCorrect().button
            } else {
                score -= 1
                alertTitle = AlertItem.flagWrong().title
                alertMessage = AlertItem.flagWrong(score: score).message
                alertButton = AlertItem.flagWrong().button
            }
            showingAlert = true

            answer = Bool.random() // <- This is only to give different results
        }

        func gameOver() {
            alertTitle = AlertItem.finishedGame().title
            alertMessage = AlertItem.finishedGame(score: score).message
            alertButton = AlertItem.finishedGame().button

            showingAlert = true
        }

        func generalAlert() {
            alertTitle = AlertItem.generalAlert.title
            alertMessage = AlertItem.generalAlert.message
            alertButton = AlertItem.generalAlert.button
        }
    }
}

This will make your View have only one alert and no logic

struct ContentView: View {
    @StateObject private var vm = ViewModel()

    var body: some View {
        VStack {
            Button("Show Alert Flag Tapped", action: vm.flagTapped)
                .buttonStyle(.bordered)
                .padding()

            Button("Show Alert Finished", action: vm.gameOver)
                .buttonStyle(.bordered)
                .padding(.bottom)

            Button("Show General Alert", action: vm.generalAlert)
                .buttonStyle(.bordered)

        }
        .alert(vm.alertTitle, isPresented: $vm.showingAlert) {
            Button(vm.alertButton) { }
        } message: {
            vm.alertMessage
        }
    }
}

I use this as easy to add new and manage alert messages because they are all in the same place

   

Very nice and professional approach, Nigel. On which day of 100 days of SwiftUI are you? Hope I could code like this soon...

   

I have finished it (twice!). but I am sure that you will get there. Sometimes write code that works then look at how to refactor to more managable section. Like the alert if only had one in project then the above code would not be worth doing, however found that quite often alerts are need in code but only when the unhappy route is taken but they just clutter up code for the "happy" route. I used to used .alert(item:) but that is soon to be depreciated.

   

Save 50% in my Black Friday sale.

SAVE 50% To celebrate WWDC22, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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

Reply to this topic…

You need to create an account or log in to reply.

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.