Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

How to use FormatStyle to restrict TextField input in SwiftUI

A custom FormatStyle can help you control the allowed characters of a SwiftUI TextField. You might want to allow numbers only or a specific set of characters. While you could use a formatter in many cases, it’s good to know there’s a flexible solution using a custom FormatStyle implementation.

In my case, I was looking for a solution to allow integers within closed-range bounds only. I’ve found several solutions, but none felt coherent or readable. By adopting a custom FormatStyle, I was able to simplify the code at the implementation level and increase readability and reusability.

Reasons for limiting TextField input

Before we dive into the code examples, I’d like to introduce you to the scenario I had to solve. For my Xcode Simulator developer tool RocketSim, I’ve found a crash that happened when the grid block size was set to 0 or smaller:

Using a custom FormatStyle, I could limit TextField input to non-zero integers.
Using a custom FormatStyle, I could limit TextField input to non-zero integers.

I’ve tried finding satisfying solutions, but many suggested using a slightly ugly onChange handler for this case:

TextField("", value: $gridBlockSize, format: .number)
    .onChange(of: gridBlockSize, perform: { _ in
        let filtered = max(gridBlockSize, 1)
        if filtered != gridBlockSize {
            gridBlockSize = filtered
        }
    })

The configured number format constrained input to numbers only, and the input got constrained, but the code needed to look more reusable and readable to me. While Stack Overflow made it look like many developers accepted this solution, I decided to continue my journey since it needed to satisfy me more.

Stay updated with the latest in SwiftUI

The 2nd largest newsletter in the Apple development community with 18,598 developers. Don't miss out – Join today:


Creating a custom FormatStyle

By creating a custom FormatStyle, I was able to change the code as follows:

TextField("", value: $gridBlockSize, format: .ranged(1...Int.max))

I’ve created a custom format that uses a closed range to validate integer inputs. The FormatStyle protocol requires defining a FormatInput and FormatOutput associated type, for which the TextField constrains the output to be a string.

In our case, we make use of the ParseableFormatStyle protocol that inherits from FormatStyle:

struct RangeIntegerStyle: ParseableFormatStyle {

    var parseStrategy: RangeIntegerStrategy = .init()
    let range: ClosedRange<Int>

    func format(_ value: Int) -> String {
        let constrainedValue = min(max(value, range.lowerBound), range.upperBound)
        return "\(constrainedValue)"
    }
}

TextFields uses the parsing strategy to convert a string value into the FormatInput type. In our case, we need to be able to convert a string value back into an integer:

struct RangeIntegerStrategy: ParseStrategy {
    func parse(_ value: String) throws -> Int {
        return Int(value) ?? 1
    }
}

By making use of static member lookup, we improve discoverability for autocompletion:

/// Allow writing `.ranged(0...5)` instead of `RangeIntegerStyle(range: 0...5)`.
extension FormatStyle where Self == RangeIntegerStyle {
    static func ranged(_ range: ClosedRange<Int>) -> RangeIntegerStyle {
        return RangeIntegerStyle(range: range)
    }
}

Our RangeIntegerStyle is relatively simple, but you could use this technique and rewrite it to constraint input to a specific set of characters.

Making use of existing Formatters

A custom FormatStyle gives you all the freedom to limit input to any set of characters. While this is great, you might want to benefit from existing formatters in other cases.

The ranged integer constraint could be created using a number formatter as well:

let numberFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    formatter.minimum = .init(integerLiteral: 1)
    formatter.maximum = .init(integerLiteral: Int.max)
    formatter.generatesDecimalNumbers = false
    formatter.maximumFractionDigits = 0
    return formatter
}()

TextField("", value: $gridBlockSize, formatter: numberFormatter)

The Foundation framework provides several concrete subclasses of Formatter that you can use in many cases. It depends on the issue you’re trying to solve, but either a formatter or custom FormatStyle should be able to help create what you need.

Conclusion

A custom FormatStyle can help you constrain input for text fields in SwiftUI. While standard formatters solve many cases, you might have more specific requirements, like limiting input to a particular set of characters. Readability and reusability improve over making use of often recommended on-change handlers.

If you want to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!