iosswiftswiftui

How to use TextField with an optional String in SwiftUI using a ParseableFormatStyle?


I’m building a SwiftUI form where I’m using TextField with optional bindings. Here’s a simplified version of what I have:

import SwiftUI

struct ContentView: View {
    
    @State private var name: PersonNameComponents? = nil
    @State private var age: Int? = nil
    @State private var password: String? = nil
    
    var body: some View {
        NavigationStack {
            List {
                TextField("Enter your full name", value: $name, format: .name(style: .medium))
                TextField("Enter your age", value: $age, format: .number)
                // This line doesn't compile
                TextField("Enter your password", value: $password, format: <#T##ParseableFormatStyle#>)
            }
        }
    }
}

As you can see, I’m using optional @State variables for name, age, and password. The first two work great because .name(style:) and .number are valid ParseableFormatStyles for their respective types. However, I’m hitting a wall when it comes to the third TextField.

I want to keep password as an optional String (String?) and use the TextField(_:value:format:) initializer, but there doesn’t seem to be a built-in ParseableFormatStyle that works with String.

I’ve seen solutions that use the TextField(_:text:) initializer with a non-optional String (@State var password: String = ""), or even workarounds using a custom Binding with get: and set: closures. However, I’d really like to avoid those approaches for the sake of consistency and simplicity, as I want this to work the same way the other two fields do.

Is there a clean way to make this work with an optional String using TextField(_:value:format:), or is this just not supported?


Solution

  • You can either create your own ParseableFormatStyle that formats an optional string, or write a computed property on Optional<String> and access it through the Binding. The latter is basically writing a custom Binding using init(get:set:), but more readable.

    The ParseableFormatStyle can be written like this:

    extension ParseableFormatStyle where Self == NilAsEmptyStringFormat {
        static var nilAsEmptyString: NilAsEmptyStringFormat { .init() }
    }
    
    struct NilAsEmptyStringFormat: ParseableFormatStyle {
        struct ParseStrategy: Foundation.ParseStrategy {
            func parse(_ value: String) -> String? {
                value.isEmpty ? nil : value
            }
        }
        
        func format(_ value: String?) -> String {
            value ?? ""
        }
        
        let parseStrategy: ParseStrategy
        
        init() {
            parseStrategy = .init()
        }
    }
    
    // Usage:
    
    TextField("Enter your password", value: $password, format: .nilAsEmptyString)
    

    The property extension is simpler:

    extension String? {
        var nilAsEmpty: String {
            get { self ?? "" }
            set { self = newValue.isEmpty ? nil : newValue  }
        }
    }
    
    // Usage:
    
    TextField("Enter your password", text: $password.nilAsEmpty)