Per un’app Mac con righe modificabili a riga singola, gestisci Return con onSubmit, inserisci un nuovo modello subito sotto la riga corrente e porta il focus sul suo campo usando FocusState. Assegna a ogni riga un ID stabile, così l’inserimento mantiene il collegamento tra testo, riga e focus.
Questo tutorial crea un piccolo editor di elenchi locale con voci sintetiche come Item A. Premendo Return si crea una riga vuota sotto il campo in modifica, si mantiene il testo esistente e si sposta il focus della tastiera sul nuovo campo. Il testo non viene diviso nel punto di inserimento.
Quale controllo di testo supporta questo comportamento?
Usa un TextField a riga singola quando ogni riga rappresenta una voce breve. Apple documenta esplicitamente che un TextField attiva la propria azione onSubmit quando l’utente preme il tasto hardware o software di invio. Apple documenta anche che assegnare un valore a FocusState sposta il focus sulla vista associata. Entrambe le API sono disponibili da macOS 12. Apple: onSubmit(of:_:), Apple: FocusState.
Scegli il controllo in base a ciò che l’utente sta modificando:
| Attività di modifica | Punto di partenza | Comportamento di Return da implementare |
|---|---|---|
| Una voce breve per riga | TextField a riga singola |
Inviare, inserire sotto, portare il focus sulla nuova riga |
| Un paragrafo in ogni riga | TextEditor o una vista di testo AppKit |
Definire in che modo inserire una riga differisce dall’inserire un a capo |
| Un documento con comandi di testo personalizzati | NSTextView incorporata in SwiftUI |
Gestire il comando di testo pertinente e coordinare le modifiche al modello |
Apple descrive TextEditor come un editor multiriga scorrevole. La documentazione non stabilisce per questo controllo lo stesso comportamento di invio con Return documentato per TextField. Di conseguenza, per modificare l’esempio e usare TextEditor occorre rivedere la gestione dei comandi; aggiungere soltanto lo stesso modificatore non è un equivalente documentato. Apple: TextEditor, Apple: onSubmit(of:_:).
Mantieni l’identità della riga distinta dalla sua posizione
Tre valori svolgono funzioni diverse: l’ID della riga identifica la voce, il suo indice nell’array determina dove inserire e l’ID del focus identifica il campo che deve ricevere l’input da tastiera.
ForEach di Apple crea viste a partire da dati identificabili. Usa un modello Identifiable con un ID assegnato quando viene creata la riga. In questa soluzione, lo stesso ID diventa anche il valore di focus della riga. Questo collegamento permette a una voce di mantenere la propria identità quando se ne inserisce un’altra sopra. Apple: ForEach.
Evita di usare il testo modificabile come ID: due righe vuote avrebbero la stessa identità e la modifica cambierebbe quell’identità. Anche le posizioni nell’array sono identità poco adatte a questa operazione di inserimento, perché si spostano. Queste sono conseguenze del modello di dati proposto, non ulteriori requisiti di SwiftUI.
Ogni campo ha bisogno di un valore di focus distinto. Apple avverte che associare lo stesso valore di focus a più viste crea un’associazione di focus ambigua. Memorizzare un UUID per ogni riga offre un modo semplice per distinguere i campi. Apple: FocusState.
Crea l’esempio completo per macOS
In Xcode crea un progetto App per macOS che usa SwiftUI e imposta come destinazione di distribuzione macOS 12 o successivo. Sostituisci il punto di ingresso dell’app e la vista dei contenuti generati con il seguente file singolo, rimuovendo la dichiarazione originale dell’app affinché il target contenga un solo punto di ingresso @main.
L’esempio usa un normale VStack all’interno di una vista di scorrimento. Registra la nuova riga come destinazione di focus in attesa, poi ne richiede il focus quando il campo appare. È una strategia di implementazione derivata dalle API di focus e di apparizione di Apple; Apple non documenta le callback di apparizione come garanzia universale che il focus sia pronto. Il momento esatto di onAppear dipende dalla vista. Apple: onAppear(perform:).
import SwiftUI
import Foundation
import Dispatch
private struct EditableRow: Identifiable {
let id = UUID()
var text: String
}
@main
struct RowEntryDemoApp: App {
var body: some Scene {
WindowGroup {
RowEditor()
}
}
}
private struct RowEditor: View {
@State private var rows = [
EditableRow(text: "Item A"),
EditableRow(text: "Item B")
]
@State private var pendingFocusID: UUID?
@FocusState private var focusedRowID: UUID?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Local row editor")
.font(.headline)
Text("Press Return in a field to insert a row below it.")
.font(.subheadline)
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 10) {
ForEach($rows) { $row in
TextField("New item", text: $row.text)
.textFieldStyle(.roundedBorder)
.focused($focusedRowID, equals: row.id)
.onSubmit {
insertRow(after: row.id)
}
.id(row.id)
.onAppear {
focusIfPending(row.id) {
proxy.scrollTo(row.id, anchor: .center)
}
}
}
}
.padding(4)
}
}
}
.padding(20)
.frame(minWidth: 420, minHeight: 280)
}
private func insertRow(after id: UUID) {
guard pendingFocusID == nil,
let index = rows.firstIndex(where: { $0.id == id })
else { return }
let newRow = EditableRow(text: "")
pendingFocusID = newRow.id
rows.insert(newRow, at: index + 1)
}
private func focusIfPending(
_ id: UUID,
scroll: @escaping () -> Void
) {
guard pendingFocusID == id else { return }
DispatchQueue.main.async {
guard pendingFocusID == id,
rows.contains(where: { $0.id == id })
else { return }
scroll()
focusedRowID = id
pendingFocusID = nil
}
}
}
Il modello contiene tutti i dati di esempio e il file definisce ogni tipo e funzione a cui si fa riferimento. Nell’ambiente di ricerca non erano disponibili un compilatore Swift e un runtime macOS, quindi il programma è stato esaminato ma non compilato né eseguito.
Come interagiscono inserimento e focus
L’associazione della stringa del campo aggiorna la riga corrispondente mentre l’utente modifica il testo. Perciò, il gestore dell’invio può inserire una nuova riga senza sostituire o cancellare la voce corrente. Apple documenta gli aggiornamenti continui per i campi di testo associati a una stringa. Apple: TextField.
Il gestore dell’inserimento individua tramite ID la riga da cui proviene l’invio e inserisce l’elemento in index + 1. Per una riga intermedia, la nuova voce viene inserita prima della voce esistente successiva. Per l’ultima riga, la posizione coincide con endIndex dell’array, una posizione valida per l’inserimento che aggiunge l’elemento in fondo. Apple: insert(_:at:).
La callback di apparizione del nuovo campo verifica se il suo ID corrisponde alla richiesta in attesa. Il blocco accodato esegue un nuovo controllo prima di scorrere e assegnare il focus. Questi controlli impediscono che una callback di apparizione non pertinente consumi la richiesta. L’esempio ignora inoltre gli invii aggiuntivi finché è in attesa una richiesta di focus.
L’invio differito aggiuntivo rimanda il lavoro al di fuori della callback di apparizione; non è una barriera di rendering documentata. Verifica questa scelta di pianificazione sulle versioni di macOS supportate e con la gerarchia di contenitori effettiva.
Lo scorrimento e il focus sono operazioni distinte. scrollTo individua un elemento figlio tramite il suo identificatore di vista e lo porta nell’area visibile; assegnare focusedRowID richiede il focus della tastiera. Il .id(row.id) esplicito indica la destinazione dello scorrimento. Apple: scrollTo(_:anchor:).
Verifica il risultato e individua i problemi comuni
Segui questa sequenza di accettazione con le voci di esempio:
- Fai clic su
Item A, digita altro testo e premiReturnuna volta. - Verifica che compaia un campo vuoto tra le due voci originali.
- Digita subito
Item Ce verifica che compaia nel nuovo campo. - Premi
Returnnell’ultimo campo e verifica che compaia una nuova riga in fondo. - Premi
Returnin un campo vuoto e verifica che sia possibile aggiungere un’altra riga vuota. - Continua finché non è necessario scorrere, poi verifica che il campo inserito diventi visibile e accetti la digitazione.
Questi sono i risultati attesi da verificare localmente. L’esempio consente intenzionalmente le voci vuote e mantiene l’intera stringa corrente, anche quando il punto di inserimento si trova al suo interno. Se il tuo caso d’uso richiede di dividere la stringa in due voci, occorre gestire anche la selezione e gli intervalli di testo.
Se compare una riga ma la digitazione continua nel vecchio campo, controlla l’ID del focus, la callback di apparizione del nuovo campo e se la finestra è attiva. Verifica anche che un’altra callback non reimposti il focus. Evita di rigenerare gli ID delle righe durante il calcolo della vista.
Se l’inserimento avviene due volte, controlla i gestori di invio nelle viste di livello superiore. Apple consente onSubmit sui singoli controlli e sulle viste contenitore, e documenta submitScope come metodo per impedire che l’invio si propaghi ai gestori di livello superiore. Affida l’azione di inserimento a un solo gestore. Apple: onSubmit(of:_:).
Cambiare l’etichetta di invio non implementa la navigazione o l’inserimento. Apple definisce submitLabel come impostazione dell’etichetta semantica di invio. La modifica del modello e l’assegnazione del focus richiedono comunque codice dedicato. Apple: submitLabel(_:).
Per raccolte molto più grandi, rivaluta lo stack eager. Apple osserva che contenitori come List e LazyVStack interrogano in modo differito gli elementi di ForEach. Potrebbe quindi essere necessario portare un campo fuori schermo nell’area visibile prima che la sua callback di apparizione possa completare la richiesta di focus. Non sostituire il contenitore senza verificare questa sequenza. Apple: ForEach.
Quando usare invece AppKit e TextKit?
Usa l’esempio SwiftUI per voci brevi e indipendenti. Quando le righe contengono testo su più righe o richiedono una gestione precisa dei comandi di testo, puoi incorporare una tua NSTextView tramite NSViewRepresentable. Apple documenta i representable come il ponte per creare e aggiornare viste AppKit in SwiftUI, con un coordinatore che inoltra le interazioni delegate. Apple: NSViewRepresentable.
Per questa soluzione alternativa, mantieni lo stesso modello basato sugli ID delle righe e implementa la sequenza seguente:
- Crea la vista di testo e assegna un coordinatore come delegate.
- Inoltra le modifiche al testo al modello della riga corrispondente.
- Implementa
textView(_:doCommandBy:)e riconosci il comandoinsertNewline(_:)secondo il comportamento scelto per l’editor. - Quando gestisci quel comando come inserimento di una riga, mantieni il testo corrente, inserisci la nuova riga e restituisci
trueper acquisire il comando. Restituiscifalseper i comandi che il delegate non gestisce. - Quando la nuova vista di testo è collegata alla finestra che la contiene, richiedi il focus tramite il metodo
makeFirstResponder(_:)di quella finestra.
Apple documenta sia il comando di nuova riga sia il contratto Boolean del delegate: true indica che il delegate ha gestito il comando, mentre false consente alla vista di testo di provarci. Questo fornisce un percorso esplicito per la gestione dei comandi in un editor gestito dall’app. Apple: insertNewline(_:), Apple: textView(_:doCommandBy:).
Mantieni la composizione del testo attiva quando aggiungi la gestione di comandi personalizzati. AppKit espone hasMarkedText() per segnalare il testo contrassegnato; usa questo stato per decidere se il delegate deve lasciare un comando al sistema di testo. Esegui prove di composizione nell’editor risultante, senza presumere che ogni interazione relativa a Return debba inserire una riga. Apple: hasMarkedText().
Per il focus, Apple indica esplicitamente di usare makeFirstResponder(_:) della finestra contenitore, invece di chiamare direttamente becomeFirstResponder() della vista di testo. L’accesso al layout di TextKit ha inoltre una conseguenza specifica per versione: da macOS 12, accedere esplicitamente a layoutManager di una vista di testo la commuta in modalità di compatibilità con NSLayoutManager. L’inserimento di righe non richiede questo accesso. Apple: NSTextView.
Per l’attività a riga singola, parti dall’esempio SwiftUI completo e verifica nel tuo Mac il flusso di inserimento, visualizzazione e digitazione. Un’identità stabile, una posizione di inserimento esplicita e una richiesta di focus mirata forniscono il comportamento fondamentale; passa a una vista di testo AppKit quando l’attività di modifica richiede gli strumenti di gestione dei comandi e della selezione.
Fonti
- onSubmit(of:_:) — Apple
- FocusState — Apple
- TextField — Apple
- TextEditor — Apple
- ForEach — Apple
- onAppear(perform:) — Apple
- insert(_:at:) — Apple
- scrollTo(_:anchor:) — Apple
- submitLabel(_:) — Apple
- NSViewRepresentable — Apple
- insertNewline(_:) — Apple
- textView(_:doCommandBy:) — Apple
- hasMarkedText() — Apple
- NSTextView — Apple