Per rendere cliccabile su Mac lo spazio vuoto all’interno di una riga SwiftUI, estendi il contenuto della riga fino alla larghezza disponibile e applica una forma di contenuto rettangolare. Per una riga che esegue un’azione, inserisci entrambi i modificatori nell’etichetta di un Button.
Questo tutorial crea una piccola app macOS le cui righe contengono forme e testo sintetici. Facendo clic su una forma, sulla relativa etichetta o sullo spazio vuoto accanto, viene eseguita la stessa azione della riga. Lo spazio esterno alla riga non rientra nell’area di attivazione dell’azione.
Perché il testo risponde, ma lo spazio vuoto no?
Layout e hit testing rispondono a domande diverse. Un frame determina come una vista occupa lo spazio proposto; una forma di contenuto definisce l’area usata per l’hit testing. Apple documenta separatamente questi aspetti nel modificatore per i frame flessibili e nel modificatore per le forme di contenuto.
Per esempio, una riga può sembrare larga perché il suo contenitore riempie una finestra, mentre l’interazione è associata solo a una vista di testo breve. Allo stesso modo, aggiungere spazio vuoto al layout non specifica in modo esplicito che l’intera riga debba partecipare all’hit testing. La soluzione pratica consiste nello stabilire prima i limiti desiderati della riga, quindi assegnare a quell’area una forma di interazione.
Usa .frame(maxWidth: .infinity, alignment: .leading) per permettere all’etichetta di espandersi quando il contenitore padre propone una larghezza maggiore. Poi applica .contentShape(.interaction, Rectangle()) a quell’etichetta espansa. Apple definisce il tipo di forma di contenuto interaction specificamente per l’hit testing e l’accessibilità.
Questo non rende la riga infinitamente larga. Il frame continua a partecipare alla proposta di layout del contenitore padre. Un contenitore stretto produce una riga stretta e una forma di contenuto non può fornire lo spazio di layout mancante. Questa combinazione soddisfa due requisiti: un’etichetta sufficientemente larga e un’area di hit testing esplicita.
Crea un esempio completo di pulsante-riga
Crea in Xcode un progetto di app macOS con SwiftUI e Swift. Imposta la versione minima di macOS supportata su macOS 12 o successiva per usare l’overload esplicito di contentShape con .interaction, disponibile, secondo Apple, a partire da macOS 12. Sostituisci la dichiarazione dell’app e la vista del contenuto generate con il programma in un unico file riportato di seguito; mantieni una sola dichiarazione di app @main. Per la disponibilità, consulta la documentazione di Apple sull’overload di contentShape.
L’esempio usa un Button perché ogni riga avvia un’azione. Apple consiglia i pulsanti per gli elementi dell’interfaccia che eseguono azioni e supporta esplicitamente l’uso di viste arbitrarie come etichette dei pulsanti. Nella documentazione sui gesti di tocco, Apple consiglia inoltre un pulsante personalizzato quando un controllo basato su gesti sarebbe funzionalmente equivalente a un pulsante.
import SwiftUI
@main
struct ClickableRowsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct SampleItem: Identifiable {
let id: Int
let title: String
let color: Color
}
struct ContentView: View {
private let items = [
SampleItem(id: 1, title: "Blue square", color: .blue),
SampleItem(id: 2, title: "Orange square", color: .orange),
SampleItem(id: 3, title: "Purple square", color: .purple)
]
@State private var lastActivated = "None"
@State private var activationCount = 0
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Choose a shape")
.font(.headline)
ScrollView {
VStack(spacing: 8) {
ForEach(items) { item in
Button {
lastActivated = item.title
activationCount += 1
} label: {
HStack(spacing: 12) {
Rectangle()
.fill(item.color)
.frame(width: 24, height: 24)
Text(item.title)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.frame(maxWidth: .infinity,
alignment: .leading)
.contentShape(.interaction, Rectangle())
}
.buttonStyle(.plain)
.background(Color.gray.opacity(0.12))
}
}
}
Text("Last activated: \(lastActivated)")
Text("Activations: \(activationCount)")
}
.padding(16)
.frame(minWidth: 360, minHeight: 280)
}
}
Tutti gli elementi e lo stato dell’interazione rimangono in memoria. Ogni elemento ha un identificatore intero fisso, coerente con il protocollo Identifiable di Swift, che rappresenta un’identità stabile. La vista padre possiede lo stato di attivazione, seguendo le indicazioni di Apple secondo cui lo State locale dovrebbe rimanere privato nella vista che deve possederlo.
Avvia l’app e fai clic nell’area vuota a destra di un titolo qualsiasi. Il conteggio delle attivazioni dovrebbe aumentare di uno e lo stato dovrebbe indicare il nome della riga. Lo sfondo grigio rende visibile l’area prevista della riga durante questa verifica; la forma di contenuto esplicita definisce l’area di interazione.
Questo programma è stato esaminato confrontandolo con la documentazione API recuperata, ma qui non è stato compilato né eseguito perché non era disponibile una toolchain Swift. Verifica il comportamento con puntatore e tastiera nella tua versione di macOS prima di integrarlo.
Dove vanno inseriti frame, padding e forma di contenuto?
In questo esempio, l’ordine è intenzionale: crea il contenuto dell’etichetta, aggiungi il padding interno, espandi l’etichetta risultante e definisci la forma di interazione su quell’area. Tutte queste operazioni avvengono nella closure dell’etichetta del pulsante. Apple documenta che l’ordine dei modificatori è importante.
Inserendo il padding all’interno dell’etichetta con la forma, l’area imbottita viene inclusa nell’area di attivazione prevista. Inserendo invece il padding all’esterno del pulsante si crea spazio di layout circostante: non presumere che tale spazio appartenga all’etichetta interattiva del pulsante. Analogamente, espandere soltanto un contenitore esterno non definisce la stessa geometria dell’etichetta ottenuta espandendo il contenuto all’interno del pulsante.
Il modificatore .buttonStyle(.plain) mantiene semplice l’aspetto dell’esempio. Apple descrive lo stile plain dei pulsanti come privo di decorazioni quando inattivo, ma con possibili effetti negli stati premuto, attivo o abilitato. È una scelta di stile, non un sostituto della definizione della larghezza e della forma di contenuto dell’etichetta.
Per una versione minima di macOS precedente alla 12, sostituisci .contentShape(.interaction, Rectangle()) con .contentShape(Rectangle()). Apple documenta che l’overload precedente definisce la forma per l’hit testing ed è disponibile a partire da macOS 10.15. Questa sostituzione modifica il requisito relativo all’API per la forma di contenuto; verifica separatamente le altre API usate dall’app.
Una riga dovrebbe usare un pulsante, la selezione di un elenco o un gesto?
Scegli l’interazione in base al significato del clic sulla riga. Rendere reattivo lo spazio vuoto dovrebbe preservare quel significato e la titolarità degli eventuali controlli presenti nella riga.
| Comportamento visibile all’utente | Punto di partenza appropriato | Limite da preservare |
|---|---|---|
| L’intera riga esegue un’unica azione | Un Button con etichetta espansa e dotata di forma |
Mantieni un solo proprietario dell’azione per la riga |
| La riga rappresenta l’elemento selezionato | Una List con un binding alla selezione |
Lascia che sia l’elenco a gestire la selezione |
| Una vista personalizzata richiede un gesto di clic distinto | Una vista sagomata con onTapGesture |
Associa il gesto all’area prevista |
| La riga contiene più controlli indipendenti | Controlli distinti e affiancati | Assegna a ogni controllo la propria area di attivazione |
Per la selezione in un elenco, usa il modello di selezione del contenitore prima di aggiungere un gestore di clic all’intera riga. La documentazione di Apple su List descrive un binding a un identificatore di elemento facoltativo per la selezione singola e a un insieme di identificatori per la selezione multipla. Un pulsante che esegue un’azione e una riga di elenco che diventa selezionata rispondono a requisiti di interazione diversi.
Un gesto è appropriato quando serve specificamente il riconoscimento dei gesti su una vista personalizzata. Applica la stessa sequenza geometrica—padding e frame, poi forma di contenuto—prima di aggiungere onTapGesture. Apple documenta il parametro count come il numero di tocchi o clic richiesti. Una forma di contenuto fornisce l’area di attivazione, ma da sola non fornisce un’azione. Vedi onTapGesture(count:perform:).
Se la riga contiene anche un pulsante o un interruttore separato, mantienilo fuori dall’etichetta del pulsante principale. Una struttura utile prevede uno stack orizzontale con un pulsante principale espandibile e un controllo separato sulla destra. Lo spazio vuoto nell’etichetta principale appartiene alla sua azione; il controllo sulla destra possiede la propria area. Evita di annidare controlli interattivi all’interno di un pulsante che racchiude l’intera riga.
Non aggiungere modificatori di priorità dei gesti solo per correggere l’attivazione dello spazio vuoto. Apple documenta highPriorityGesture come un modificatore che cambia la precedenza, mentre simultaneousGesture permette ai gesti di essere elaborati insieme. Queste operazioni cambiano le relazioni tra i gesti e possono influire sulle interazioni delle viste discendenti; non definiscono la geometria mancante della riga.
Individua il motivo per cui la riga continua a non ricevere clic
Segui questa sequenza sulla versione più semplice della riga. Ti aiuta a distinguere un problema di dimensionamento da uno relativo alla forma dell’area di attivazione o alla sovrapposizione di viste.
- Controlla i limiti effettivi dell’etichetta. Aggiungi temporaneamente
.border(Color.orange)subito dopo il modificatore del frame dell’etichetta. Se il bordo termina accanto al testo, controlla la proposta di larghezza del contenitore padre e gli eventuali vincoli di larghezza fissa. - Controlla la posizione dei modificatori. Verifica che la forma di contenuto copra l’etichetta espansa, incluso il padding che vuoi rendere cliccabile.
- Controlla chi gestisce l’interazione. Per una riga-pulsante, usa l’azione del pulsante. Rimuovi temporaneamente i gestori di tocco ridondanti sulla riga mentre isoli il problema.
- Controlla le decorazioni sovrapposte. Esamina gli overlay e le viste affiancate che si trovano sopra la riga. Per le decorazioni che non devono mai ricevere input, valuta di applicare
.allowsHitTesting(false)solo a quelle decorazioni. - Ripristina gli altri controlli uno alla volta. Dopo aver ripristinato ciascun controllo, prova la sua area e poi verifica lo spazio vuoto rimanente.
Apple documenta allowsHitTesting(_:) come un modificatore che controlla se una vista partecipa alle operazioni di hit testing. Applicarlo all’intera riga la escluderebbe dalla partecipazione; se questo è il comportamento desiderato, applicalo alla vista decorativa specifica.
Usa alcuni punti distinti per verificare il risultato. La tabella descrive il comportamento previsto per il programma fornito, non risultati di test misurati.
| Punto del clic | Risultato previsto |
|---|---|
| Quadrato colorato o titolo della riga | Attiva quella riga una volta |
| Spazio vuoto a destra, all’interno della riga grigia | Attiva quella riga una volta |
| Padding interno sopra o sotto il titolo | Attiva quella riga una volta |
| Spazio tra le righe | Nessuna attivazione della riga |
| Padding esterno o area vuota sotto le righe | Nessuna attivazione della riga |
Ridimensiona la finestra e ripeti la verifica dello spazio vuoto. Verifica anche il focus e l’attivazione da tastiera con le impostazioni di navigazione da tastiera del tuo Mac. Questi controlli aiutano a stabilire se il componente finale si comporta come il controllo previsto, oltre a rispondere semplicemente al puntatore.
Quando è rilevante l’hit testing di AppKit?
Per una normale riga SwiftUI, parti dalle API di SwiftUI per layout, forme di contenuto e controlli. AppKit diventa pertinente quando l’app include viste native personalizzate o una vista incorporata che modifica la destinazione degli eventi.
Apple documenta NSView.hitTest(_:) come un metodo che individua il discendente più profondo contenente un punto. Una finestra lo usa principalmente per determinare quale vista riceve un evento di pressione del mouse. Il punto di input è espresso nel sistema di coordinate della supervista e Apple afferma che l’invocazione diretta è raramente necessaria.
Se gestisci una vista AppKit personalizzata, esamina i suoi limiti e l’override di hit testing in corrispondenza di quel confine. Una forma di contenuto SwiftUI non è un’istruzione per ignorare la titolarità degli eventi di una vista nativa separata. Mantieni la correzione nella gerarchia di viste responsabile di ricevere il clic.
Per la riga di questo tutorial, la procedura supportata è semplice: fai in modo che l’etichetta del pulsante occupi la larghezza prevista della riga, definisci una forma di interazione rettangolare e mantieni gli altri controlli nelle rispettive aree. In questo modo lo spazio vuoto utile diventa cliccabile, mentre le altre interazioni della finestra restano intatte.
Fonti
- contentShape(_:eoFill:) — Apple
- contentShape(::eoFill:) — Apple
- interaction — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- Button — Apple
- plain — Apple
- padding(::) — Apple
- Identifiable — Apple
- State — Apple
- List — Apple
- onTapGesture(count:perform:) — Apple
- highPriorityGesture(_:including:) — Apple
- simultaneousGesture(_:including:) — Apple
- allowsHitTesting(_:) — Apple
- hitTest(_:) — Apple