To insert a suggestion without applying the field’s value, keep completion and submission separate. On macOS 15 or later, SwiftUI provides native text suggestions; use submitScope() to block ancestor submit handlers and an explicit button for the final action.

This tutorial builds a small label picker inside your own Mac app. Typing Bl offers synthetic labels such as Blue Tile. Choosing a suggestion updates the editable draft; clicking Apply Label copies that draft into the applied value. The distinction matters: preventing an app action and consuming a Return-key command are different requirements.

Choose the API that matches the interaction

Apple’s textInputSuggestions(_:) modifier presents choices for a text field. Each actionable suggestion needs textInputCompletion(_:), which supplies its replacement string. Both APIs are documented for macOS 15 and later. A suggestion without a completion modifier can appear but does not respond to clicks. Apple: textInputSuggestions(_:), Apple: textInputCompletion(_:)

For a short label field, replacing the entire draft is appropriate. Apple’s suggestions documentation describes that whole-field replacement. Do not assume this API provides a caret-relative token editor or an inline suffix preview; those require a more specific editing design. Apple: textInputSuggestions(_:)

Your requirement Suitable starting point
Offer complete labels on macOS 15 or later SwiftUI native text suggestions
Prevent the field from invoking an ancestor submit handler submitScope() on the field
Use a custom suggestion layout or support an earlier deployment target App-owned suggestion buttons that update the draft binding
Make Return accept a highlighted item and consume that command An owned AppKit control with command handling
Preview a suffix or replace the word at the insertion point AppKit text completion and an explicit completion range

The last two rows are architectural choices derived from the documented command and completion APIs. They are useful when the interaction requires more control than a replacement string and a submit boundary provide.

Build a complete SwiftUI label picker

Create a macOS App project in Xcode with SwiftUI, using an SDK that includes macOS 15 APIs. Set the deployment target to macOS 15 or later. Replace the generated app and content-view declarations with the following single source file, leaving only one @main app declaration in the target.

The example keeps draft and appliedLabel separate. Its local matching rule is deliberately simple: case-sensitive prefix matching, no suggestions for empty text, and no exact-match suggestion. Swift documents hasPrefix(_:) as a string-prefix comparison; broader matching rules would be an application choice. Swift: Strings and Characters

import SwiftUI

@main
struct LabelCompletionApp: App {
    var body: some Scene {
        WindowGroup {
            LabelCompletionView()
        }
    }
}

struct LabelCompletionView: View {
    private let labels = [
        "Blue Tile",
        "Blue Tray",
        "Green Tile",
        "Round Tray"
    ]

    @State private var draft = ""
    @State private var appliedLabel = ""
    @State private var applyCount = 0
    @FocusState private var fieldIsFocused: Bool

    private var matches: [String] {
        guard !draft.isEmpty else { return [] }
        return labels.filter {
            $0.hasPrefix(draft) && $0 != draft
        }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Choose a label")
                .font(.headline)

            TextField("Type Bl to see suggestions", text: $draft)
                .textFieldStyle(.roundedBorder)
                .focused($fieldIsFocused)
                .textInputSuggestions {
                    ForEach(matches, id: \.self) { label in
                        Text(label)
                            .textInputCompletion(label)
                    }
                }
                .submitScope()

            Button("Apply Label") {
                appliedLabel = draft
                applyCount += 1
            }
            .disabled(draft.isEmpty)

            Text("Draft: \(draft)")
            Text("Applied: \(appliedLabel)")
            Text("Apply count: \(applyCount)")
        }
        .padding(24)
        .frame(width: 420)
        .onAppear {
            fieldIsFocused = true
        }
    }
}

This is a complete app example, with its sample labels and state included. It was inspected against the retrieved API documentation, but it was not compiled or run here because a Swift toolchain and macOS UI runtime were unavailable.

Why suggestion selection does not apply the label

Each completion supplies a string for the text field, whose binding points to draft. The only code that assigns appliedLabel or increments applyCount is the Apply Label button action. Updating the draft therefore has no route to that final action in this example.

The field also has submitScope(). Apple documents this modifier as preventing submission triggers from reaching submission modifiers higher in the view hierarchy. This matters when the component is later embedded in a larger form with an ancestor onSubmit. Apple: submitScope(_:)

Initial focus comes from @FocusState. Apple documents that assigning its bound value programmatically moves focus to the associated view. Focus is an editing convenience here; it does not control whether the label is applied. Apple: FocusState

What the example intentionally allows

The label list provides suggestions rather than restricting valid input. Typing Square Tile still enables Apply Label. If your interface permits only listed labels, change the validation rule explicitly—for example, enable the button only when the draft equals a catalog entry. Do not silently replace an unmatched draft with the first suggestion.

The example also replaces the whole label. If the draft contains multiple words, choosing a completion should be understood as choosing the finished field value. A token editor that must preserve surrounding text needs selection-aware replacement instead.

Understand what “without submitting” means

SwiftUI’s onSubmit(of:_:) documentation says a text field triggers its submit action when the user presses Return. Submit actions can be attached to an individual view or a larger hierarchy. Inspect both locations when an autocomplete selection appears to trigger an unexpected app action. Apple: onSubmit(of:_:)

submitScope() establishes a boundary for ancestor handlers. It does not document a promise that no internal submit event occurs, that editing remains active, or that every keyboard command is consumed. Likewise, Apple’s completion modifiers document text replacement, not a universal suppression of Return-key behavior. Keep those limits in mind when choosing the interaction. Apple: submitScope(_:), Apple: textInputCompletion(_:)

For an existing component, inspect these paths separately:

  1. Remove any final application action from the field’s own onSubmit if Return should never apply its value.
  2. Put submitScope() on the field to block an ancestor submit action.
  3. Check button keyboard shortcuts separately, especially an explicit default action.
  4. Check any change observer or model setter that performs the final action whenever the draft changes.

An empty local onSubmit closure is not the documented replacement for a submit boundary. Apple specifically provides submitScope() to stop propagation. If the app should submit on Return only when no completion is active, that conditional keyboard policy needs additional state and command handling. Apple: onSubmit(of:_:)

Use AppKit when Return must accept and stop

For an owned NSTextField, AppKit provides control(_:textView:doCommandBy:). Its text-view argument is the control’s field editor. The delegate can handle a bound command and return true; returning false leaves it unhandled by that delegate. This is the documented extension point for customizing commands in a text field. Apple: control(_:textView:doCommandBy:)

A custom completion controller can use that extension point with this sequence:

  1. Track whether its suggestion list is open and which item is highlighted.
  2. When the Return command arrives with a valid highlighted item, insert that item into the draft.
  3. Close the suggestion list and synchronize the app’s draft model.
  4. Return true because the controller handled the command.
  5. Return false for commands the controller does not handle.

This sequence is an implementation design derived from the delegate contract, not a built-in autocomplete policy. Decide separately what Return should do when the list is closed. If you own a standalone NSTextView instead, the corresponding hook is textView(_:doCommandBy:); Apple states that returning true prevents the text view from performing that command itself. Apple: textView(_:doCommandBy:)

For text completion itself, NSTextView.insertCompletion(_:forPartialWordRange:movement:isFinal:) supports replacement and selection changes. With isFinal: false, it selects the proposed suffix; with isFinal: true, it collapses the selection after the completed word. The flag describes completion state, not application submission. The movement argument identifies how completion navigation or selection occurred. Apple: insertCompletion(_:forPartialWordRange:movement:isFinal:)

Preserve the correct text range

AppKit’s rangeForUserCompletion identifies the partial word from its beginning to the insertion point. Use a range suited to the intended replacement and recalculate it after edits or caret movement. A previously captured range should not be reused blindly after the text changes. Apple: rangeForUserCompletion

When bridging text ranges, do not substitute Swift’s character count for an NSString length. Swift’s documentation explains that String counts extended grapheme clusters, while NSString length counts UTF-16 code units. Those values can differ. Whole-field binding replacement in the SwiftUI example avoids this range conversion entirely. Swift: Strings and Characters)

Verify insertion and application separately

Use the visible draft, applied value, and counter to check the outcome on each supported macOS version. The following are expected checks for the example, not reported test results.

Action Expected observable result
Type Bl The draft is Bl; matching labels are offered
Click Blue Tile The draft becomes Blue Tile; applied value and count stay unchanged
Press Return while editing The app does not apply the draft
Click Apply Label The applied value copies the draft; count increases once
Edit the draft afterward The previous applied value remains visible
Clear the draft Suggestions are empty and Apply Label is disabled
Type bl No matches under the example’s case-sensitive rule

Also exercise keyboard selection in the native suggestion list and repeat the checks after embedding the field in its real parent view. Apple notes that events such as moving a macOS window can dismiss the native suggestion interface, so the draft should remain usable without that list. Apple: textInputSuggestions(_:)

If your deployment target predates macOS 15, provide an availability-gated native view and a fallback, or use app-owned suggestion buttons throughout. A fallback button can assign its label directly to the draft and leave the final action on Apply Label. That provides the same draft/application separation, while keyboard navigation and presentation remain work for your component.

For this label-picker task, the useful boundary is observable: accepting a completion changes the editable draft, and only the explicit final action changes the applied label. Use SwiftUI’s native suggestions and submit scope for that boundary; move to AppKit command handling when consuming Return itself is part of the requirement.

Sources