Per fare clic su un controllo SwiftUI attraverso un buco trasparente in una vista sovrapposta, assegna alla vista superiore una forma di hit-test esplicita che escluda il buco. Questa procedura costruisce quell'interazione all'interno di un'app Mac, utilizzando un anello in memoria e un pulsante sotto di esso.
L'obiettivo concreto è una piccola interfaccia di disegno desktop: fare clic sull'anello arancione seleziona l'anello; fare clic sul suo centro trasparente attiva il controllo blu visibile sotto di esso. Entrambi i livelli appartengono alla stessa gerarchia di viste SwiftUI. L'esempio utilizza una geometria condivisa per il disegno e l'hit testing, quindi non necessita di file immagine o campionamento di pixel.
Scegli la regola di hit-test prima di modificare la gestione degli eventi
Apple documenta due controlli SwiftUI rilevanti. allowsHitTesting(_:) determina se una vista partecipa all'hit testing. contentShape(_:eoFill:) specifica la forma utilizzata per l'hit testing. Usa il primo per decorazioni che non dovrebbero mai ricevere clic; usa il secondo per una sovrapposizione interattiva con buchi. La documentazione sulla partecipazione all'hit-testing di Apple e la documentazione su content-shape descrivono queste responsabilità separate.
| Requisito del livello superiore | Scelta di implementazione |
|---|---|
| La decorazione deve ignorare ogni clic | Applica .allowsHitTesting(false) a quella decorazione |
| Un cerchio deve rispondere all'interno del suo contorno | Assegna alla sua vista interattiva una content shape circolare |
| Un anello deve rispondere intorno a un centro trasparente | Usa una forma composta e riempimento even-odd per disegno e hit testing |
| Un sottalbero AppKit proprietario necessita di partecipazione selettiva | Esegui l'override del suo confine hitTest(_:) e rifiuta i punti fuori dalla sua regione di interazione |
Queste API descrivono partecipazione e geometria; non specificano un test automatico dell'alfa del pixel finale renderizzato. Per forme sintetiche, il percorso supportato è descrivere esplicitamente la regione di interazione prevista. Un aspetto trasparente da solo è quindi una specifica di interazione incompleta. Questa distinzione deriva dalle interfacce documentate basate su Boolean e forme, piuttosto che da una promessa su ogni modificatore di rendering.
Costruisci un esempio completo di anello-sopra-pulsante in SwiftUI
Crea un'app macOS in Xcode usando SwiftUI e imposta il deployment target su macOS 12 o successivo. Sostituisci la dichiarazione dell'app generata e la vista iniziale con il programma completo seguente, lasciando solo una dichiarazione @main nel target. L'esempio usa .contentShape(.interaction, …), il cui tipo di interazione è disponibile da macOS 12 e controlla esplicitamente l'hit testing. La documentazione sull'interazione di Apple identifica quel comportamento e la disponibilità.
L'anello contiene due sottopercorsi ellittici concentrici. Disegno e interazione usano entrambi la regola even-odd, rendendo l'ellisse interna un buco. Apple documenta Shape.path(in:) come la geometria all'interno del rettangolo fornito e FillStyle come configurazione di rendering, incluso il riempimento even-odd. Geometria delle forme e FillStyle forniscono i contratti sottostanti.
import SwiftUI
struct RingShape: Shape {
func path(in rect: CGRect) -> Path {
let side = min(rect.width, rect.height)
let outer = CGRect(
x: rect.midX - side / 2,
y: rect.midY - side / 2,
width: side,
height: side
)
let inner = outer.insetBy(
dx: side * 0.30,
dy: side * 0.30
)
var path = Path()
path.addEllipse(in: outer)
path.addEllipse(in: inner)
return path
}
}
struct RoutingDemo: View {
@State private var lowerClicks = 0
@State private var ringClicks = 0
var body: some View {
VStack(spacing: 18) {
Text("Click the center or the orange ring")
ZStack {
Button {
lowerClicks += 1
} label: {
Rectangle()
.fill(Color.blue)
.frame(width: 240, height: 240)
.overlay {
Text("Below")
.foregroundColor(.white)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
RingShape()
.fill(Color.orange, style: FillStyle(eoFill: true))
.frame(width: 240, height: 240)
.contentShape(.interaction, RingShape(), eoFill: true)
.onTapGesture {
ringClicks += 1
}
Rectangle()
.stroke(Color.gray, lineWidth: 1)
.frame(width: 240, height: 240)
.allowsHitTesting(false)
}
.frame(width: 240, height: 240)
Text("Below: \(lowerClicks) | Ring: \(ringClicks)")
}
.padding(24)
}
}
@main
struct TransparentRoutingApp: App {
var body: some Scene {
WindowGroup {
RoutingDemo()
}
}
}
Il pulsante inferiore rimane responsabile della propria azione. Il gesto dell'anello incrementa un contatore diverso. Queste closure finali esprimono le due azioni direttamente; la guida del linguaggio Swift documenta le closure come funzionalità che possono catturare i valori circostanti. The Swift Programming Language: Closures spiega la sintassi e il comportamento di cattura.
Il contorno grigio è decorazione, quindi il suo hit testing è disabilitato individualmente. Mantieni quel modificatore sul contorno: applicarlo a un contenitore che contiene i livelli interattivi darebbe al contenitore la politica di partecipazione sbagliata. L'anello stesso deve rimanere idoneo all'interazione sulla sua banda arancione.
Perché la stessa regola di riempimento è importante due volte
La chiamata di disegno e il modificatore content-shape ricevono entrambi eoFill: true. Impostarlo solo per il disegno descriverebbe il buco visibile senza dare esplicitamente all'hit testing la stessa regola. Apple documenta il parametro content-shape separatamente dal rendering, quindi entrambe le dichiarazioni appartengono a questo esempio. Il modificatore content-shape definisce la forma di interazione e la sua regola di avvolgimento.
Mantieni allineate anche le dimensioni e le trasformazioni. Qui, il disegno dell'anello e la content shape condividono un quadrato di 240 punti. Se in seguito sposti o ridimensioni l'intera vista anello, mantieni quelle operazioni insieme. Se invece cambi solo un percorso di disegno dentro un contenitore più grande, aggiorna la geometria di interazione per descrivere la stessa regione prevista.
Verifica l'instradamento con tre clic specifici
Attiva la finestra dell'app, quindi fai clic ben lontano dai confini delle forme. I seguenti punti sono coordinate locali illustrative all'interno del quadrato di 240 punti, misurate dal suo angolo in alto a sinistra. Distinguono il buco centrale, la banda arancione e l'angolo trasparente senza fare affidamento sui pixel di bordo antialiasati.
| Punto | Geometria visibile del livello superiore | Cambio contatore atteso |
|---|---|---|
(120, 120) |
Buco centrale | Below aumenta di una unità |
(210, 120) |
Banda arancione | Ring aumenta di una unità |
(10, 10) |
Fuori dal cerchio esterno dell'anello | Below aumenta di una unità |
Il raggio esterno è 120 punti. L'inset interno è 72 punti per lato, lasciando un raggio interno di 48 punti. I tre punti di prova sono rispettivamente a 0, 90 e circa 155,6 punti dal centro. Ho verificato questo calcolo geometrico deterministico con Python; l'app SwiftUI è stata ispezionata ma non è stata compilata o eseguita in questo ambiente perché non era disponibile una toolchain Swift.
Questo test è anche una sequenza diagnostica utile. Se la banda arancione non risponde, ispeziona il gesto dell'anello e i modificatori di partecipazione. Se il centro attiva l'anello, ispeziona la sua content shape e il parametro even-odd. Se nessun livello risponde nel buco, rimuovi temporaneamente le sovrapposizioni aggiuntive, poi ripristinale una alla volta per identificare quale livello cambia l'instradamento.
Per controlli geometrici diretti nel tuo codice, Path.contains(_:eoFill:) supporta la stessa scelta even-odd. Passa un punto espresso nel sistema di coordinate del percorso che hai costruito. Questo permette a un test geometrico di porre la stessa domanda dentro-o-fuori senza trattare uno screenshot come modello di interazione. La documentazione sul contenimento del percorso di Apple descrive questa operazione.
Usa l'hit testing di AppKit solo a un confine che possiedi
Quando una sovrapposizione è già implementata in AppKit, NSView.hitTest(_:) è il punto di personalizzazione documentato. Restituisce il discendente selezionato per un punto ed è usato principalmente per scegliere il destinatario del mouse-down. Il suo punto di input è nelle coordinate della superview del ricevitore, quindi convertilo prima di confrontarlo con la geometria locale. La documentazione di Apple su NSView hitTest specifica questi dettagli.
Per una vista personalizzata proprietaria, l'override può rifiutare i punti fuori dalla sua regione di interazione e deferire i punti accettati a super.hitTest(_:). Quello che segue è un frammento di metodo per una sottoclasse NSView personalizzata esistente, piuttosto che una seconda app completa. Usa la RingShape definita sopra e richiede import AppKit accanto a import SwiftUI.
override func hitTest(_ point: NSPoint) -> NSView? {
guard let parent = superview else {
return super.hitTest(point)
}
let localPoint = convert(point, from: parent)
let region = RingShape().path(in: bounds)
guard region.contains(localPoint, eoFill: true) else {
return nil
}
return super.hitTest(point)
}
Usa questo frammento solo dove il contenuto visibile della vista personalizzata e il suo sottalbero seguono effettivamente quella geometria ad anello. Restituire nil rifiuta quel sottalbero per il punto; non nomina una destinazione né comanda a SwiftUI di attivare un particolare sibling. Se il livello inferiore previsto possa ricevere il clic dipende ancora dalla gerarchia circostante. Una vista contenitrice che continua ad accettare il punto può rimanere un ostacolo.
Quando incorpori quella vista AppKit proprietaria in SwiftUI, usa NSViewRepresentable per crearla e aggiornarne la configurazione. Mantieni lo stato degli elementi e la geometria di interazione sincronizzati attraverso il metodo update del representable; usa un coordinator quando fai il bridging di messaggi delegate o target-action. Apple indica anche agli sviluppatori di lasciare frame e bounds della vista root rappresentata sotto il controllo del layout di SwiftUI. La documentazione di Apple su NSViewRepresentable descrive questi confini di ciclo di vita e proprietà.
Preserva l'evento originale e il proprietario del gesto
Esegui la decisione di instradamento durante l'hit testing. Evita di intercettare un clic sul livello superiore e invocare manualmente l'azione del controllo inferiore: ciò sostituirebbe un comando dell'applicazione con l'interazione normale del controllo inferiore. Analogamente, questo esempio non necessita di reposting di eventi, traversamento delle viste di implementazione di SwiftUI o sostituzione del gestore di gesti di un livello inferiore. Ogni livello interattivo visibile possiede il proprio comportamento.
L'ambito qui è l'ordinaria attivazione del pulsante primario all'interno della gerarchia delle viste dell'app. Un clic centrale riuscito non stabilisce comportamento per scrolling, trascinamento, hover o menu contestuali; testali separatamente se la tua interfaccia li aggiunge. In particolare, Apple distingue la content shape di interazione dalle forme di anteprima per drag e context menu, che non modificano l'hit testing. La documentazione di Apple su content-shape spiega quei tipi separati.
Per questo compito desktop, mantieni il percorso dell'anello come definizione condivisa della sua banda visibile e regione cliccabile. Ciò dà al centro trasparente un significato preciso: il livello superiore rifiuta il punto, e il controllo SwiftUI inferiore rimane responsabile di rispondere attraverso il normale sistema di interazione.
Fonti
allowsHitTesting(_:) — Apple
contentShape(_:eoFill:) — Apple
interaction — Apple
path(in:) — Apple
FillStyle — Apple
Closures — Swift.org
contentShape(::eoFill:) — Apple
contains(_:eoFill:) — Apple
hitTest(_:) — Apple
NSViewRepresentable — Apple