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

Day 25 Milestone Challenge Project: Finished

Forums > 100 Days of SwiftUI

For those who didn't read all Hints provided, here is a rather dorky solution that adopts a "if then" logic. Not elegant, but it works.

struct ContentView: View {
    @State private var botMoves = ["circle.fill", "hand.raised", "scissors"] //rock, paper, scissors
    @State private var appMove = Int.random(in: 0...2) //the app's current choice, used along with botMoves.
    @State private var winOrLose = Bool.random() //decides if the player should pick win or lose. True is win false is lose.
    @State private var totalPlayed = 0
    @State private var showingScore = false
    @State private var scoreTitle = ""
    @State private var userScore = 0
    @State private var gameFinished = false //whether the game is finished at 8 questions

    var currentMove : String {
        botMoves[appMove]
    }
    let botMoveStrings = ["rock", "paper", "scissors"]

    var body: some View {
        VStack(spacing: 15) {
            Spacer()
            Text("")
            Text("Rock Paper Scissors").font(.largeTitle.weight(.bold)).foregroundColor(.blue)
            Spacer()
            Text("The system's current move is: \(currentMove == "circle.fill" ? "rock" : currentMove == "hand.raised" ? "paper" : "scissors")")
            Spacer()
            Text("You want to: \(winOrLose ? "Win" : "Lose")")

            VStack {
                Spacer()
                ForEach(0..<3) {number in
                    Button {
                        tapped(number)
                    } label: {
                        NumberImage(move: botMoves[number]).padding(20)
                    }
                }
                .alert(scoreTitle, isPresented: $showingScore) {
                    Button("Continue", action: askQuestion)
                } message: {
                    Text("Your socre is \(userScore)")
                }.buttonStyle(.borderedProminent).tint(.mint)
                Spacer()
                Button("Restart") {
                    gameFinished = true
                    }
                    .alert("You have finished all \(totalPlayed) games", isPresented: $gameFinished) {
                        Button("Restart", action: reset)
                        //Button("Cancel", role: .cancel) {} //add a cancel option/button
                    } message: {
                        Text("Your final score is \(userScore)")
                    }
                Spacer()
                Text("Your score is: \(userScore)")
                Spacer()
                Text("Your have played \(totalPlayed) games")
                Spacer()
            }
        }
        .ignoresSafeArea()
    }

    //resets the game by shuffling up botMoves and picking a new correct answer
    func askQuestion() {
        botMoves.shuffle()
        winOrLose = Bool.random()
        appMove = Int.random(in: 0...2)
    }
    func reset() {
        userScore = 0
        totalPlayed = 0
        botMoves.shuffle()
    }

    func tapped(_ number: Int) {
        if totalPlayed == (10 - 1) {
            gameFinished = true
        }
        else {
            if winOrLose == true {
                if currentMove == "circle.fill" && botMoves[number] == "hand.raised" {
                    userScore += 1
                    scoreTitle = "Correct! You got 1 point"
                }
                else if currentMove == "hand.raised" && botMoves[number] == "scissors" {
                    userScore += 1
                    scoreTitle = "Correct! You got 1 point"
                }
                else if currentMove == "scissors" && botMoves[number] == "circle.fill" {
                    userScore += 1
                    scoreTitle = "Correct! You got 1 point"
                }
                else {
                    userScore -= 1
                    scoreTitle = "Wrong! You lost 1 point"
                }
            }
            else if winOrLose == false {
                if currentMove == "circle.fill" && botMoves[number] == "scissors" {
                    userScore += 1
                    scoreTitle = "Correct! You got 1 point"
                }
                else if currentMove == "hand.raised" && botMoves[number] == "circle.fill" {
                    userScore += 1
                    scoreTitle = "Correct! You got 1 point"
                }
                else if currentMove == "scissors" && botMoves[number] == "hand.raised" {
                    userScore += 1
                    scoreTitle = "Correct! You got 1 point"
                }
                else {
                    userScore -= 1
                    scoreTitle = "Wrong! You lost 1 point"
                }
            }
        }
        totalPlayed += 1
        showingScore = true
    }
}

struct NumberImage: View {
    var move: String

    var body: some View {
        Image(systemName: move).renderingMode(.original).clipShape(Capsule()).shadow(radius: 5)
    }
}

   

Thanks for posting, it was helpful when I was struggling with the tapped function and the ternary operator in the text view. Based on Paul's hint about having an array for winning moves, I was able to simplify the logic somewhat, though it took me a bit longer than it should have 😭

My code is below, but first a question for anyone who knows: When using ForEach to create three buttons, I first tried iterating through the array with ForEach(choices, id: \.self) {Text($0)} but kept getting an error, so I ended up just using the range 0..<3. What was I doing wrong?

Lastly, my buttonTapped function isn't quite right: on the 10th/final guess, if I get the correct answer, it doesn't add 1 to my score. Anyone know what the solution is?

struct ContentView: View {
    @State private var choices = ["🪨", "📄", "✂️"] // array of choices for the app to choose from
    @State private var choicesWin = ["📄", "✂️", "🪨"] // array of choices to win against the app
    @State private var choicesLose = ["✂️", "🪨", "📄"] // array of choices to lose against the app
    @State private var appChoice = Int.random(in: 0...2) // randomly select from the choices array
    @State private var shouldWin = Bool.random() // bool that will toggle between true (win) and false (lose) each turn
    @State private var showingScore = false // for alert when to show score
    @State private var userScore = 0 // track user's score and display in alert
    @State private var questionNumber = 0 // track question count
    @State private var gameFinished = false // for alert when game is finished after 10 questions
    @State private var scoreTitle = "" // for alert title

    var currentMove: String { // computed variable of the app's current move
        choices[appChoice]
    }
    var winningMove: String { // computed variable of the winning move
        choicesWin[appChoice]
    }
    var losingMove: String { // computed variable of the losing move
        choicesLose[appChoice]
    }

    var body: some View {
        VStack(spacing: 15) {
            Spacer()
            Text("Rock, paper, scissors!")
                .font(.largeTitle.weight(.heavy))
                .padding(.bottom, 30)
            Text("Your score: \(userScore)")
            Text("Total guesses: \(questionNumber)")
            Text("The computer chose \(currentMove) and wants you to \(shouldWin ? "win" : "lose").")
                .font(.title.weight(.regular))
                .padding(30)
                .padding(.bottom, 20)
            ForEach(0..<3) { number in // three buttons based on the items in the choices array
                Button {
                    buttonTapped(number)
                } label: {
                    Text(choices[number])
                }
                .font(.system(size: 60))
            }
            Spacer()

        }
        .alert(scoreTitle, isPresented: $showingScore) { // alert to show the score after each question
            Button("Continue", action: askQuestion)
        }
        .alert(scoreTitle, isPresented: $gameFinished) { // alert to show the final score when game is finished
            Button("Restart", action: reset)
        } message: {
            Text("Final score: \(userScore)/\(questionNumber)")
        }
    }
    func askQuestion() {
        choices.shuffle()
        shouldWin.toggle()
        appChoice = Int.random(in: 0...2)
    }
    func reset() {
        userScore = 0
        questionNumber = 0
        choices.shuffle()
        showingScore = false
    }

    func buttonTapped(_ number: Int) { // code to run when a button is tapped
        if questionNumber == 9 {
            scoreTitle = "Finished!"
            gameFinished = true
        }
        else {
            if shouldWin == true {
                if winningMove == choices[number] {
                    userScore += 1
                    scoreTitle = "Correct! You got 1 point"
                    showingScore = true
                } else {
                    scoreTitle = "Wrong!"
                    showingScore = true
                }
            }
            else if shouldWin == false {
                if losingMove == choices[number] {
                    userScore += 1
                    scoreTitle = "Correct! You got 1 point"
                    showingScore = true
                } else {
                    scoreTitle = "Wrong!"
                    showingScore = true
                }
            }
        }
        questionNumber += 1
    }
}

   

When using ForEach to create three buttons, I first tried iterating through the array with ForEach(choices, id: \.self) {Text($0)} but kept getting an error, so I ended up just using the range 0..<3. What was I doing wrong?

Depends on what the error was.

   

@roosterboy, this chunk of code returned two errors. Let me know if you need more info.

ForEach(choices, id: \.self) { number in // three buttons based on the items in the choices array
      Button {
          buttonTapped(number) // error on this line: Cannot convert value of type 'String' to expected argument type 'Int'
      } label: { // error on this line: Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body
          Text($0)
      }

   

My code above had a few bugs, so I'm reposting it. To resolve the problem of a point not being added if the 10th/final guess was correct, I split the buttonTapped function into two functions: the first to check for the correct answer and +1 to the score if correct and the second to check if questionNumber == 10 is true, which will tell us if the game is finished.

struct ContentView: View {
    @State private var choices = ["🪨", "📄", "✂️"] // array of choices for the app to choose from
    @State private var choicesWin = ["📄", "✂️", "🪨"] // array of choices to win against the app
    @State private var choicesLose = ["✂️", "🪨", "📄"] // array of choices to lose against the app
    @State private var appChoice = Int.random(in: 0...2) // randomly select from the choices array
    @State private var shouldWin = Bool.random() // bool that will toggle between true (win) and false (lose) each turn
    @State private var showingScore = false // for alert when to show score
    @State private var userScore = 0 // track user's score and display in alert
    @State private var questionNumber = 0 // track question count
    @State private var gameFinished = false // for alert when game is finished after 10 questions
    @State private var scoreTitle = "" // for alert title

    var currentMove: String { // computed variable of the app's current move
        choices[appChoice]
    }
    var winningMove: String { // computed variable of the winning move
        choicesWin[appChoice]
    }
    var losingMove: String { // computed variable of the losing move
        choicesLose[appChoice]
    }

    var body: some View {
        VStack(spacing: 15) {
            Spacer()
            Text("Rock, paper, scissors!")
                .font(.largeTitle.weight(.heavy))
                .padding(.bottom, 30)
            Text("Your score: \(userScore)")
            Text("Total guesses: \(questionNumber)")
            Text("The computer chose \(currentMove) and wants you to \(shouldWin ? "win" : "lose").")
                .font(.title.weight(.regular))
                .padding(30)
                .padding(.bottom, 20)
            ForEach(0..<3) { number in // three buttons based on the items in the choices array
                Button {
                    buttonTapped(number)
                    isGameFinished()
                } label: {
                    Text(choices[number])
                }
                .font(.system(size: 60))
            }
            Spacer()

        }
        .alert(scoreTitle, isPresented: $showingScore) { // alert to show the score after each question
            Button("Continue", action: askQuestion)
        }
        .alert(scoreTitle, isPresented: $gameFinished) { // alert to show the final score when game is finished
            Button("Restart", action: reset)
        } message: {
            Text("Final score: \(userScore)/\(questionNumber)")
        }
    }
    func askQuestion() {
        //choices.shuffle() removed this line because I think it was messing up my choicesWin and choicesLose arrays
        shouldWin = Bool.random()
        appChoice = Int.random(in: 0...2)
    }
    func reset() {
        userScore = 0
        questionNumber = 0
        //choices.shuffle() removed this line; see above
        showingScore = false
    }

    func buttonTapped(_ number: Int) { // first function to run when a button is tapped
        if shouldWin == true {
            if winningMove == choices[number] {
                userScore += 1
                scoreTitle = "Correct! You got 1 point"
                showingScore = true
            } else {
                scoreTitle = "Wrong!"
                showingScore = true
            }
        }
        else if shouldWin == false {
            if losingMove == choices[number] {
                userScore += 1
                scoreTitle = "Correct! You got 1 point"
                showingScore = true
            } else {
                scoreTitle = "Wrong!"
                showingScore = true
            }
        }
        questionNumber += 1
    }

    func isGameFinished() { // second function to run when button is tapped
        if questionNumber == 10 {
            showingScore = false
            scoreTitle = "Finished!"
            gameFinished = true
        } else {
            // do nothing
        }
    }
}

   

Alex asks:

When using ForEach to create three buttons, I first tried iterating through the array with ForEach(choices, id: .self) {Text($0)} but kept getting an error, so I ended up just using the range 0..<3. What was I doing wrong?

Part of your SwiftUI journey is learning the code. Another KEY part is learning to read the error codes.

Think of ForEach as a factory. Its job is to create views from raw materials that you provide. You want your ForEach factory to create some buttons. So ask yourself, what raw materials do you need to provide to the Button?

private var choices = ["🪨", "📄", "✂️"]  //  Always ask yourself, what TYPE is this variable??:

// Here you want the ForEach to build some views for you. 
// ForEach is a factory. You give it something and it builds a view for each item you give it.\
// What are the RAW materials you are sending to this factory?
ForEach(choices, id: \.self) { number in // What TYPE is choices?
      Button {
      // The error message is VERY clear here.
      // Your factory machine expects an Int, but you are providing a String
          buttonTapped(number) // error on this line: Cannot convert value of type 'String' to expected argument type 'Int'
      } label: {
          Text($0)
      }

I think you tripped yourself. In the ForEach declaration, you are sending in three strings. Yet, you call the internal variable number. After that, in your brain, you think you're working with a number. This is NOT what you're working with. Then you further confuse yourself by asking "Why doesn't buttonTapped(number) work?" It doesn't work because you are NOT giving it a number.

Think about using specific variable names. Be creative! What would be a better name than number? Maybe gamePiece, or playersOption?

1      

@Obelix, thanks for your advice. After some testing, I think I got it working. I changed number to option, which is a String, since I'm iterating through the options of the choices array, each of which is a string. I then adjusted the buttonTapped function, replacing _ number: Int with _ option: String and choices[number] with option. The ForEach block is below:

 ForEach(choices, id: \.self) { option in // three buttons based on the options in the choices array
                Button {
                    buttonTapped(option)
                    isGameFinished()
                } label: {
                    Text("\(option)")

Using Text($0) returns an error ("Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body"), so I think I need to review what that syntax actually means. But Text("\(option)") seems to work.

   

Alex finds another error:

Using Text($0) returns an error ("Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body"), so I think I need to review what that syntax actually means. But Text("(option)") seems to work.

Another error, another opportunity to learn how to interpret error messages! I agree, though. This one is not so straight forward. Let's have a closer look.

The error messages says:

closure type '() -> Text' expects 0 arguments.

Swift thinks you defined a closure that takes NO arguments, and returns a Text. Did you?

Did you provide a closure that takes ZERO arguments? By using the $0 argument, you're telling Swift that you're not going to use named arguments and that Swift should take care of this for you.

However, you declared your closure requires a String (specifically one of the elements in the choices array) and that you have named this parameter option.

 ForEach(choices, id: \.self) { option in // Tell Swift this closure requires a parameter you have named: option
                Button {
                    buttonTapped(option)
                    isGameFinished()
                } label: {
                    Text($0)  // Here you're telling Swift this closure does not accept parameters
}

So look again at the error message? Does it make sense now? The compiler is telling you the exact reason for the error. You provide a parameter named option, yet in the body of the closure you used the $0 syntax indicating your closure shouldn't accept named parameters.

   

Hacking with Swift is sponsored by Emerge

SPONSORED Why are Swift reference types bad for app startup time, and what’s the performance cost of protocol conformances? That’s just a couple of the topics you can learn about on the Emerge blog — written by the app performance experts behind Emerge’s advanced app optimization and monitoring tools, based on their experience of working at companies like Apple, Airbnb, Snap, and Spotify.

Find out more

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.