Om du vill infoga ett förslag utan att tillämpa fältets värde ska du hålla komplettering och skickning åtskilda. Från och med macOS 15 tillhandahåller SwiftUI inbyggda textförslag; använd submitScope() för att blockera överordnade hanterare för skickning och en uttrycklig knapp för den slutliga åtgärden.

Den här handledningen bygger en liten etikettväljare i din egen Mac-app. När du skriver Bl visas syntetiska etiketter som Blue Tile. Om du väljer ett förslag uppdateras det redigerbara utkastet; om du klickar på Apply Label kopieras utkastet till det tillämpade värdet. Skillnaden är viktig: att förhindra en appåtgärd och fånga upp ett kommando från Returtangenten är olika krav.

Välj API efter interaktionen

Apples modifierare textInputSuggestions(_:) visar val för ett textfält. Varje förslag som går att välja behöver textInputCompletion(_:), som anger ersättningssträngen. Båda API:erna är dokumenterade för macOS 15 och senare. Ett förslag utan en kompletteringsmodifierare kan visas, men reagerar inte på klick. Apple: textInputSuggestions(_:), Apple: textInputCompletion(_:)

För ett kort etikettfält är det lämpligt att ersätta hela utkastet. Apples dokumentation om förslag beskriver denna ersättning av hela fältet. Utgå inte från att detta API tillhandahåller en tokenredigerare som utgår från markörens position eller en förhandsvisning av ett infogat suffix; det kräver en mer specifik redigeringslösning. Apple: textInputSuggestions(_:)

Ditt krav Lämplig utgångspunkt
Erbjuda kompletta etiketter på macOS 15 eller senare Inbyggda textförslag i SwiftUI
Förhindra att fältet anropar en överordnad hanterare för skickning submitScope() på fältet
Använda en anpassad förslagslayout eller stödja ett tidigare distributionsmål Appägda förslagsknappar som uppdaterar utkastets bindning
Låta Retur acceptera ett markerat objekt och fånga upp kommandot En egen AppKit-kontroll med kommandohantering
Förhandsvisa ett suffix eller ersätta ordet vid insättningspunkten AppKit-textkomplettering och ett uttryckligt kompletteringsintervall

De två sista raderna är arkitekturval som bygger på de dokumenterade API:erna för kommandon och komplettering. De är användbara när interaktionen kräver mer kontroll än vad en ersättningssträng och en gräns för skickning ger.

Bygg en komplett SwiftUI-etikettväljare

Skapa ett macOS App-projekt i Xcode med SwiftUI och använd ett SDK som innehåller macOS 15-API:er. Ange macOS 15 eller senare som distributionsmål. Ersätt de genererade app- och innehållsvydeklarationerna med följande enda källfil och låt endast en @main-appdeklaration finnas kvar i målet.

Exemplet håller draft och appliedLabel åtskilda. Den lokala matchningsregeln är avsiktligt enkel: skiftlägeskänslig prefixmatchning, inga förslag för tom text och inget förslag vid exakt matchning. Swift beskriver hasPrefix(_:) som en jämförelse av strängprefix; mer omfattande matchningsregler är ett val för appen. 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
        }
    }
}

Det här är ett komplett appexempel som innehåller exempel­etiketterna och tillståndet. Det granskades mot den hämtade API-dokumentationen, men kompilerades eller kördes inte här eftersom en Swift-verktygskedja och en macOS-körmiljö för gränssnitt inte var tillgängliga.

Varför val av förslag inte tillämpar etiketten

Varje komplettering tillhandahåller en sträng för textfältet, vars bindning pekar på draft. Den enda kod som tilldelar appliedLabel eller ökar applyCount finns i knappåtgärden för Apply Label. Att uppdatera utkastet leder därför inte vidare till den slutliga åtgärden i det här exemplet.

Fältet har också submitScope(). Apple dokumenterar att denna modifierare hindrar utlösare för skickning från att nå modifierare för skickning högre upp i vyhierarkin. Det är viktigt när komponenten senare bäddas in i ett större formulär med en överordnad onSubmit. Apple: submitScope(_:)

Den initiala fokuseringen styrs med @FocusState. Apple dokumenterar att en programmatisk tilldelning av dess bundna värde flyttar fokus till den tillhörande vyn. Fokus är här en bekvämlighet vid redigering; det styr inte om etiketten tillämpas. Apple: FocusState

Vad exemplet avsiktligt tillåter

Etikettlistan ger förslag i stället för att begränsa giltig inmatning. Om du skriver Square Tile är Apply Label fortfarande aktiverad. Om gränssnittet bara tillåter etiketter från listan ska du ändra valideringsregeln uttryckligen, till exempel genom att aktivera knappen bara när utkastet motsvarar en post i katalogen. Ersätt inte ett utkast som saknar matchning tyst med det första förslaget.

Exemplet ersätter också hela etiketten. Om utkastet innehåller flera ord ska val av en komplettering förstås som val av fältets färdiga värde. En tokenredigerare som måste bevara omgivande text behöver i stället ersättning som tar hänsyn till markeringen.

Förstå vad ”utan att skicka” innebär

Dokumentationen för SwiftUI:s onSubmit(of:_:) säger att ett textfält utlöser sin skickningsåtgärd när användaren trycker på Retur. Skickningsåtgärder kan kopplas till en enskild vy eller en större hierarki. Granska båda ställena när ett val av autokomplettering verkar utlösa en oväntad appåtgärd. Apple: onSubmit(of:_:)

submitScope() sätter en gräns för överordnade hanterare. Dokumentationen lovar inte att ingen intern skickningshändelse inträffar, att redigeringen förblir aktiv eller att varje tangentbordskommando fångas upp. På samma sätt beskriver Apples kompletteringsmodifierare textutbyte, inte ett universellt undertryckande av beteendet för Returtangenten. Ha dessa begränsningar i åtanke när du väljer interaktion. Apple: submitScope(_:), Apple: textInputCompletion(_:)

För en befintlig komponent ska du granska dessa delar separat:

  1. Ta bort eventuell slutlig appåtgärd från fältets egen onSubmit om Retur aldrig ska tillämpa dess värde.
  2. Lägg till submitScope() på fältet för att blockera en överordnad skickningsåtgärd.
  3. Kontrollera kortkommandon för knappar separat, särskilt en uttrycklig standardåtgärd.
  4. Kontrollera eventuella ändringsobservatörer eller modellväljare som utför den slutliga åtgärden varje gång utkastet ändras.

En tom lokal onSubmit-stängning är inte den dokumenterade ersättningen för en gräns för skickning. Apple tillhandahåller uttryckligen submitScope() för att stoppa spridning. Om appen bara ska skicka vid Retur när ingen komplettering är aktiv kräver den villkorliga tangentbordspolicyn ytterligare tillstånd och kommandohantering. Apple: onSubmit(of:_:)

Använd AppKit när Retur ska acceptera och stoppa

För en egen NSTextField tillhandahåller AppKit control(_:textView:doCommandBy:). Dess textvyargument är kontrollens fältredigerare. Delegeringsobjektet kan hantera ett bundet kommando och returnera true; om det returnerar false förblir kommandot ohanterat av det delegeringsobjektet. Detta är den dokumenterade utökningspunkten för att anpassa kommandon i ett textfält. Apple: control(_:textView:doCommandBy:)

En anpassad kompletteringskontroller kan använda den utökningspunkten med följande sekvens:

  1. Håll reda på om förslagslistan är öppen och vilket objekt som är markerat.
  2. När Retur-kommandot kommer och ett giltigt objekt är markerat ska objektet infogas i utkastet.
  3. Stäng förslagslistan och synkronisera appens utkastmodell.
  4. Returnera true eftersom kontrollern hanterade kommandot.
  5. Returnera false för kommandon som kontrollern inte hanterar.

Den här sekvensen är en implementeringslösning som bygger på delegeringskontraktet, inte en inbyggd policy för autokomplettering. Bestäm separat vad Retur ska göra när listan är stängd. Om du i stället äger en fristående NSTextView är motsvarande krok textView(_:doCommandBy:); Apple anger att om true returneras hindras textvyn från att själv utföra kommandot. Apple: textView(_:doCommandBy:)

För själva textkompletteringen stöder NSTextView.insertCompletion(_:forPartialWordRange:movement:isFinal:) ersättning och ändringar av markeringen. Med isFinal: false markeras det föreslagna suffixet; med isFinal: true placeras markören efter det kompletterade ordet. Flaggan beskriver kompletteringens tillstånd, inte appens skickning. Argumentet movement anger hur navigeringen eller markeringen av kompletteringen gick till. Apple: insertCompletion(_:forPartialWordRange:movement:isFinal:)

Bevara rätt textintervall

AppKits rangeForUserCompletion identifierar delordet från dess början till insättningspunkten. Använd ett intervall som passar den avsedda ersättningen och beräkna det på nytt efter redigeringar eller förflyttning av markören. Återanvänd inte ett tidigare sparat intervall utan att kontrollera det efter att texten ändrats. Apple: rangeForUserCompletion

När du konverterar textintervall ska du inte ersätta längden för NSString med Swifts teckenantal. Swifts dokumentation förklarar att String räknar utökade grafemkluster, medan längden för NSString räknar UTF-16-kodenheter. Värdena kan skilja sig åt. Ersättning av hela fältet via bindning i SwiftUI-exemplet undviker helt den här intervallkonverteringen. Swift: Strings and Characters)

Kontrollera infogning och tillämpning separat

Använd det synliga utkastet, det tillämpade värdet och räknaren för att kontrollera resultatet på varje macOS-version som stöds. Följande är förväntade kontroller för exemplet, inte rapporterade testresultat.

Åtgärd Förväntat synligt resultat
Skriv Bl Utkastet är Bl; matchande etiketter visas som förslag
Klicka på Blue Tile Utkastet blir Blue Tile; det tillämpade värdet och räknaren förblir oförändrade
Tryck på Retur medan du redigerar Appen tillämpar inte utkastet
Klicka på Apply Label Det tillämpade värdet kopieras från utkastet; räknaren ökar en gång
Redigera utkastet efteråt Det tidigare tillämpade värdet är fortfarande synligt
Rensa utkastet Förslagen är tomma och Apply Label är inaktiverad
Skriv bl Inga matchningar enligt exemplets skiftlägeskänsliga regel

Testa även tangentbordsval i den inbyggda förslagslistan och upprepa kontrollerna efter att fältet bäddats in i sin faktiska överordnade vy. Apple påpekar att händelser som att flytta ett macOS-fönster kan stänga det inbyggda förslagsgränssnittet, så utkastet ska förbli användbart även utan listan. Apple: textInputSuggestions(_:)

Om distributionsmålet är äldre än macOS 15 ska du tillhandahålla en inbyggd vy som styrs av tillgänglighet och en reservlösning, eller använda appägda förslagsknappar överallt. En reservknapp kan tilldela sin etikett direkt till utkastet och låta den slutliga åtgärden ligga på Apply Label. Då hålls utkastet och tillämpningen åtskilda på samma sätt, medan tangentbordsnavigering och presentation hanteras av din komponent.

För den här uppgiften med en etikettväljare är den användbara gränsen synlig: när en komplettering accepteras ändras det redigerbara utkastet, och bara den uttryckliga slutåtgärden ändrar den tillämpade etiketten. Använd SwiftUIs inbyggda förslag och skickningsomfång för den gränsen; använd AppKits kommandohantering när det ingår i kravet att fånga upp själva Retur.

Källor