To keep a numeric field editable after its optional value becomes nil, preserve the editable text separately from the number. First check parsing, disabled state and focus; then use a string-backed draft when your app needs explicit recovery behavior.

This guide covers a numeric field inside a macOS app you develop: entering a synthetic Tile count, clearing it, and entering another number. The goal is to preserve a meaningful empty value while allowing the next edit.

Why did the optional number become nil?

Apple provides a SwiftUI initializer specifically for optional values: TextField(value:format:prompt:label:). Its documented behavior is to set the binding to nil when the format style cannot parse the input. A failed conversion is therefore an expected state of this API. Apple does not describe that state as disabling the field. This optional format-style initializer requires macOS 12 or later. Apple’s optional-value TextField initializer

Check which initializer your app actually uses. The older formatter: initializer has different documented failure behavior: if conversion fails, it leaves the bound value unchanged. Replacing formatter: with format: changes that contract, so treat it as a deliberate behavior change rather than a universal repair. Apple’s formatter-based TextField initializer

The underlying design issue is that an optional number cannot represent every editing state. A blank field, a partially entered minus sign and an invalid string can all lack a usable integer, but they are different text drafts. This is why a separate draft is useful: the field can retain exactly what the person is editing while the app decides when to accept a numeric value.

Diagnose the failure before changing the field

Use the following sequence in your own app window. It distinguishes a conversion problem from an interaction problem without assuming a particular macOS implementation defect.

  1. Check whether text actually changes. Click the field, select its contents and type 24. If the characters change but the numeric model does not, inspect conversion and commit handling.
  2. Check interaction conditions. Search the field and its containers for conditions such as .disabled(tileCount == nil). That condition prevents the very edit needed to recover from an empty value.
  3. Check whether the field still exists. If your view creates the editor only inside an if let branch, setting the value to nil can remove the editor. Keep its draft and editor available independently of the optional number.
  4. Check focus separately. If the field accepts a click and then typing works, restore focus through the appropriate public API.
  5. Reduce the example. Temporarily remove custom binding transformations and model synchronization. Reproduce the transition from 12 to empty to 24 in a small view.

A disabled ancestor matters: Apple documents that a container’s .disabled(true) overrides .disabled(false) on a child. Fix the ancestor’s condition instead of repeatedly enabling only the field. Apple’s disabled(_:) documentation

Also inspect custom binding setters. A setter that returns early whenever the existing number is nil cannot accept a replacement number. Likewise, mapping every failed conversion to 0 loses the distinction between an absent value and an actual zero. Those are application logic decisions; changing focus cannot repair them.

Keep an editable draft and commit the number explicitly

A string-backed field gives you control over three separate states: the current text, the accepted optional number and a validation message. Apple documents that a string-bound TextField updates its binding continuously during editing. That makes it a suitable place to retain unfinished input. Apple’s TextField overview

The complete sample below uses this policy: blank input means nil; a valid base-10 integer replaces the accepted number; invalid input remains visible and leaves the accepted number unchanged. The Clear button empties both states and requests focus so another number can be entered.

Create a macOS SwiftUI App project in Xcode with a deployment target of macOS 12 or later. Replace the generated app and content-view declarations with this single file, retaining only one @main declaration in the target. The sample uses only its own window and synthetic labels. Swift and the macOS SDK were unavailable in the research environment, so this program was inspected but not compiled or run.

import SwiftUI

@main
struct NumericEditingDemoApp: App {
    var body: some Scene {
        WindowGroup {
            NumericEditingView()
        }
    }
}

struct NumericEditingView: View {
    @State private var tileCount: Int? = 12
    @State private var draft = "12"
    @State private var message: String? = nil
    @FocusState private var fieldIsFocused: Bool

    private var acceptedDescription: String {
        if let value = tileCount {
            return String(value)
        }
        return "None"
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            TextField("Tile count", text: $draft)
                .textFieldStyle(.roundedBorder)
                .focused($fieldIsFocused)
                .onSubmit {
                    applyDraft()
                }

            HStack {
                Button("Apply") {
                    applyDraft()
                }

                Button("Clear") {
                    tileCount = nil
                    draft = ""
                    message = nil
                    fieldIsFocused = true
                }

                Button("Edit") {
                    fieldIsFocused = true
                }
            }

            Text("Accepted count: \(acceptedDescription)")

            if let message = message {
                Text(message)
                    .foregroundColor(.secondary)
            }
        }
        .padding(20)
        .frame(width: 380)
    }

    private func applyDraft() {
        if draft.isEmpty {
            tileCount = nil
            message = nil
            return
        }

        guard let parsed = Int(draft, radix: 10) else {
            message = "Enter a whole number without spaces, or clear the field."
            fieldIsFocused = true
            return
        }

        tileCount = parsed
        draft = String(parsed)
        message = nil
    }
}

The field always binds to draft, so setting tileCount to nil does not remove its editable text state. Conversion happens only when the person clicks Apply or presses Return. After an invalid submission, the message remains until the next submission or Clear; the draft remains available for correction.

The guard prevents an unsuccessful conversion from replacing the accepted number. Successful submissions normalize the spelling through String(parsed): for example, entering +024 results in 24. This is an explicit sample policy. Remove that normalization assignment if preserving the submitted spelling is useful.

Choose the numeric grammar deliberately

This example accepts signed, ungrouped base-10 integers. It rejects spaces, decimal fractions and grouping separators. Swift documents that Int(_:radix:) returns nil for invalid input or a value that the integer type cannot represent, so an oversized pasted number also follows the validation path. Swift’s Int string initializer

For a field that requires localized numeric notation, keep the same draft-and-commit structure but replace the conversion with an appropriate Foundation parse strategy. A ParseableFormatStyle exposes a parseStrategy, and a strategy’s parse(_:) method performs the conversion and can throw. Handle failure while retaining the draft. Choose matching formatting and parsing conventions, then test the intended locales. Apple’s ParseableFormatStyle documentation, Apple’s ParseStrategy documentation

If another part of your app can replace the accepted number, define how that update affects an unfinished draft. For example, a deliberate Load value action can update both states. Avoid continually rebuilding the draft from the accepted number during editing: that would discard the unfinished text this design preserves.

Restore focus through SwiftUI or AppKit

In SwiftUI, attach .focused($fieldIsFocused) to the editor and set fieldIsFocused = true from a local action, as the sample does. Apple documents that assigning a focus-state value moves focus to the associated view. Give separate fields distinct focus bindings; assigning the same focus value to multiple fields creates ambiguity. Apple’s FocusState documentation

Focus controls where editing goes. It does not change the parser, re-enable a disabled ancestor or recreate an editor removed by conditional view logic. Resolve those conditions before adding focus recovery.

For an AppKit field your app owns, inspect NSTextField.isEditable. Apple defines this property as controlling whether the user can edit the field’s value. Restore it to true if your own code incorrectly made it false, while preserving any intentional application restrictions. Apple’s NSTextField.isEditable documentation

When explicitly moving AppKit focus, check acceptsFirstResponder and call the owning window’s makeFirstResponder(_:). Its result requires care: false can mean the current responder refused to resign, while true can also occur when the proposed responder refuses and the window becomes first responder. Verify the resulting editing state. To select the whole field for replacement, selectText(_:) is the documented operation; it has no effect before the field belongs to a window’s view hierarchy. Apple’s makeFirstResponder(_:) documentation, Apple’s selectText(_:) documentation

Respect the field editor’s lifecycle

AppKit’s currentEditor() returns the control’s current field editor, or nil when it has none. Apple describes that editor as an NSTextView shared among controls in a window and created when needed. Consequently, a nil editor outside an active editing session does not establish that numeric entry is broken. Apple’s currentEditor() documentation

At the text-system level, a field editor treats Tab, Shift-Tab and Return as signals to end editing and possibly change the first responder. Do not change isFieldEditor as a numeric-conversion fix: that property governs editing behavior, not optional-number parsing. Keep these AppKit and TextKit checks confined to controls your app owns. Apple’s NSTextView.isFieldEditor documentation

Verify that clearing and re-entry both work

Run this manual sequence against the complete sample on each macOS version your app supports. These are expected results derived from the sample’s logic, not reported test executions.

Action Expected accepted value Expected editing behavior
Launch 12 Draft contains 12
Click Clear nil, displayed as None Draft is empty; focus is requested
Type 24, then click Apply 24 Draft contains 24
Replace with -, then press Return Previous accepted value Draft remains -; message appears
Replace with -8, then press Return -8 Message clears
Enter 0, then click Apply 0 Zero remains distinct from absence
Enter 2.5, then click Apply Previous accepted value Fractional text remains for correction
Empty the field, then click Apply nil Field remains available for another edit

Also test pasting an integer larger than Int.max, clicking away and returning, and clearing the field repeatedly. In an integrated app, include any action that changes the model elsewhere. These checks exercise conversion failure, focus recovery and draft synchronization separately.

The useful recovery condition is concrete: after the accepted number becomes nil, the editor remains present and interactive, and a later valid submission supplies a new number. Keeping the draft independent makes that behavior explicit without depending on undocumented changes to SwiftUI’s backing controls.

Sources