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

Day 35 Challenge - How do I make TextFields work like I want?

Forums > 100 Days of SwiftUI

Below is my code for the Multiplication Quiz challenge. What I don't like about it is how the TextField behaves. I've read numerous articles, but I can't find a way to make a TextField bound to an Integer value do the following:

  1. Start with an empty field with the cursor blinking on the right side. It seems to always have the initial value of 0 (or the last value entered, see point 2), which the user then has to delete before entering the value they want.
  2. After a value is entered and committed, I want to process the entered value, then return the field to the blank state. Instead, the previously entered value persists, despite setting the bound value back to 0, and despite fiddling with the FocusState var (suggested in one of the artilces I read).
  3. Show a numeric keypad when the TextField has focus, with a Return key on it. Can't find one of those, so other than adding a "Save" button, there's no way to commit the field (I can in Simulator because I have a physical keyboard attached, but clearly that won't work in the real world).

Any insights on how to make SwiftUI's TextField behave in a "normal" manner (i.e., as a user would expect) would be appreciated!

import SwiftUI

struct ContentView: View {
    @State private var gameInProgress = false

    @State private var table = 2
    @State private var numberOfQuestions = 5
    @State private var correct = 0
    @State private var incorrect = 0

    @State private var questions = [Int]()
    @State private var questionText = ""
    @State private var questionCounter = 0
    @State private var answer = 0

    @FocusState var isFocused: Bool

    let questionChoices = [5, 10, 20]

    var body: some View {
        NavigationView {
            VStack {
                Form {
                    Section {
                        Picker("Which table?", selection: $table) {
                            ForEach(2..<13, id: \.self) {
                                Text("\($0)")
                            }
                        }
                        .foregroundColor(gameInProgress ? .secondary : .primary)

                        Picker("How many questions?", selection: $numberOfQuestions) {
                            ForEach(questionChoices, id: \.self) { choice in
                                Text("\(choice)")
                            }
                        }
                        .foregroundColor(gameInProgress ? .secondary : .primary)
                    }
                    .disabled(gameInProgress)

                    Section {
                        HStack {
                            Spacer()
                            Button("Go") {
                                startGame()
                            }
                            .font(.title.bold())
                            Spacer()
                        }
                    }
                    .disabled(gameInProgress)

                    Section {
                        if gameInProgress {
                            HStack {
                                Text(questionText)

                                Spacer()

                                TextField("", value: $answer, formatter: NumberFormatter(), onCommit: { saveAnswer() })
                                    .frame(width: 50)
                                    .keyboardType(.numberPad)
                                    .textFieldStyle(RoundedBorderTextFieldStyle())
                                    .multilineTextAlignment(.trailing)
                                    .focused($isFocused)
                            }
                        }
                    }
                }

                HStack {
                    Text("Correct: \(correct)")
                    Spacer()
                    Text("Incorrect: \(incorrect)")
                }
                .padding()
                .foregroundColor(.mint)
            }
            .navigationTitle("Multiplication Quiz")
        }
    }

    func startGame() {
        questions.removeAll()
        for _ in 0..<numberOfQuestions {
            questions.append(Int.random(in: 2...12))
        }

        questionCounter = 0
        answer = 0
        correct = 0
        incorrect = 0
        gameInProgress.toggle()
        nextQuestion()
    }

    func nextQuestion() {
        if questionCounter > numberOfQuestions - 1 {
            gameInProgress.toggle()
            return
        }

        questionText = "Question \(questionCounter + 1): What is \(table) times \(questions[questionCounter])?"
    }

    func saveAnswer() {
        isFocused = false

        if answer == table * questions[questionCounter] {
            correct += 1
        }
        else {
            incorrect += 1
        }

        questionCounter += 1
        answer = 0
        nextQuestion()
    }
}

1      

Not sure if you've tried these hints?

See -> Fun with Number formats

1      

Thanks. That definitely helps with part of question 1 in my original post. Any further ideas on the rest of it?

1      

Jeff has more questions:

Thanks. That definitely helps with part of question 1 in my original post. Any further ideas on the rest of it?

To be honest? I've been chided about this before. So I hesitate to mention it. (See -> Obelix's Methods aren't Welcoming)

I honestly didn't give your code a thorough review. Why? Because you provided too much code that has nothing to do with your problem. I disagree with others who posted the idea below (and apparently complained to @twoStraws):

We should also not have to ask our question in some sort of special way to conform to someone elses way of thinking.

I disagree with this idea. If you have problems formatting a TextField, I suggest you stop coding your solution and fire up Playgrounds. Write a single view with a Label and TextField that accepts numeric data. Focus on solving the TextField problem. Ignore the rest.

Then show the code that doesn't work!
Show the code that you tried.
Document where you see the failures.
Document the parts that work for you and you understand.

While others have given me a hard time for stating this, I see great value in this idea. First, you're sharing your education with others, me included. (Unlike, ahem! some others...) Next, I see what you've tried; so I won't waste time suggesting non-working solutions. Also, I can look at your code and, perhaps, uncover syntax or malformed code. Finally, you get to post solutions that others will certainly use in the future. But, crivvens mate! You've included so much unrelated code, it's hard to see the specific codes what be ailin' yer heart.

Help us to help you.

PS: If you want to marvel at another example, See -> How to Toggle a Boolean with a Button?
This guy joined HWS forums today and posts ALL his source code 🤯😱, but is just wondering how to get a button to toggle a boolean. @twoStraws asks us to consider criticizing ideas not people. Maybe someone will help him? Not me! I would consider answering his question if he posted concise code that focused on his problem. I have no patience to read all that code to find his problems.

1      

But, crivvens mate! You've included so much unrelated code, it's hard to see the specific codes what be ailin' yer heart.

Ok, I see that from your perspective. The code I posted actually works, but I'm looking for alternate code that works better.

"Better" would be: 1) Cursor blinking in the textfield on initial display; 2) When the field is committed, set the bound variable back to zero and have that change reflected on screen (currently, the previously-entered value persists); 3) Show a numeric keypad that contains a return key so the user can commit the value entered (I can't find such a keyboard). I do appreciate your help, and per your request, here is some trimmed-down code that demonstrates the issues I mentioned in as few lines as possible, with the requests above repeated in comments.

import SwiftUI

struct ContentView: View {
    @State private var answer = 0
    @State private var question = 1

    @FocusState var isFocused: Bool

    let formatter: NumberFormatter = { // Thanks to your initial response, the field is now blank.
        let formatter = NumberFormatter()
        formatter.numberStyle = .none
        formatter.zeroSymbol  = ""
        return formatter
    }()

    var body: some View {
        HStack {
            Text("Question \(question):")

            Spacer()

            TextField("", value: $answer, formatter: formatter, onCommit: { saveAnswer() })  // The cursor is not blinking in this field on initial display of the View
                .frame(width: 50)
                .keyboardType(.numberPad) // I want a different keypad (if such exists) that has a return key on it
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .multilineTextAlignment(.trailing)
                .focused($isFocused)
        }
        .padding()
    }

    func saveAnswer() {
        isFocused = false // Manipulating this variable has no noticeable effect
        question += 1
        answer = 0 // Resetting this variable to zero is not reflected on-screen. The previously entered value persists.
    }
}

1      

I find that when using text in TextField, it's easy to reset it to a blank string by saying something like answer = "", but when using value, it just doesn't work with answer = 0.

So what I would do now is using text in the TextField to get user input, then convert it with Int(answer) when saving it.

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.