0

For my app I need the user to type in a number between 1 and 1000, and I would like to validate that it falls between those numbers. At first I thought of using a Stepper but then the user would have to press + a thousand times. Is there a better way to do this? I made it so that the user is prompted with a numberPad but that allows the user to input any number.

Here's my code:

@State var validityPeriod = ""

var body: some View {
    NavigationView {
        Form {
            TextField("Validity Period (In Days)", text: $validityPeriod)
        }
        .navigationBarTitle("Validation")
    }
}

I could check the number that they typed in before submitting the form, but I would prefer something that verifies the field while they're typing.

Note: I'm using Xcode 11.5 with Swift 5

Josh Correia
  • 3,807
  • 3
  • 33
  • 50

1 Answers1

2

You can check that the current value is valid via a computed Bool property. Then anywhere you want, you can place that check into your code.

SwiftUI recreates the ContentView body every time your @State variable changes (which is best to be made private, FYI). It has some intelligence in re-rendering only what's needed, but essentially your entire View is reevaluated, and your calculated bool periodIsValid retested with every keystroke.

Quite often the submit button is disabled while the form is invalid. Something like:

Button(...) {
    ...
}
.disabled(!periodIsValid)

To do it live, you can do a number of creative things with that live validation checking. Something really simple would be to make the text entered turn red if it is invalid:

struct ContentView: View {
    @State private var validityPeriod = ""

    var periodIsValid: Bool {
        guard let days = Int(validityPeriod) else { return false}
        guard days >= 1 && days <= 1000 else { return false }
        return true
    }

    var body: some View {
        NavigationView {
            Form {
                TextField("Validity Period (In Days)", text: $validityPeriod)
                    .foregroundColor(periodIsValid ? .primary : .red)
            }
            .navigationBarTitle("Validation")
        }
    }
}

Edit:

You can also limit the entry values...

Thinking more about this, you can actually perform instant-validation of the text field, which will prevent the entry of non-valid text.

To do this you use the didSet property wrapper. Only hitch is that it doesn't work with @State wrapper, so you'll have to use an @ObservedObject instead. Pretty easy...

One note is that you may want to help people understand what's going on. If your interface doesn't already make it clear what values are okay, you may want to provide a hint when you squash an invalid entry.

class ValidityPeriod: ObservableObject {
    @Published var validityPeriod = "" {
        didSet {
            // Allow blanks in the field, or they can't delete
            if validityPeriod == "" { return }

            // Check the entire value is good, otherwise ignore the change
            let days = Int(validityPeriod) ?? 0
            if days < 1 || days > 1000 { validityPeriod = oldValue }

            // Pop alert or trigger any other UI instruction here
        }
    }
}

struct ContentView: View {

    @ObservedObject var vp = ValidityPeriod()

    var body: some View {
        NavigationView {
            Form {
                TextField("Validity Period (In Days)", text: $vp.validityPeriod)
            }
            .navigationBarTitle("Validation")
        }
    }
}
Craig Temple
  • 525
  • 4
  • 10
  • 1
    `var isValidPeriod: Bool { 1...1000 ~= Int(validityPeriod) ?? 0 }` – Leo Dabus May 27 '20 at 05:25
  • Great idea to turn the text red if it's invalid. I combined your code with what I found here https://stackoverflow.com/a/58736068/7487335 which allowed me to block out letters completely. Thanks! – Josh Correia May 27 '20 at 19:35
  • 1
    Heh. We were typing at the same time. Both your posted approach and the `didSet` approach would work well. – Craig Temple May 27 '20 at 19:53