Gebruik een SwiftUI Button wanneer je aangepaste Mac-oppervlak een benoemde actie uitvoert. Gebruik een klikgebaar wanneer de interactie gebaarinformatie nodig heeft, zoals een locatie op een canvas. Deze handleiding bouwt een kleine vormeditor om te laten zien waar elk thuishoort.

Apple raadt nadrukkelijk een knop aan wanneer een element als knop functioneert. Een knop kan elke weergave als label gebruiken, dus voor een aangepaste vormgeving hoef je het interactiegedrag niet te vervangen. SwiftUI biedt standaard knopgedrag, waaronder toegankelijkheidslabels en hints. Gebaren

Welke interactie heeft je oppervlak nodig?

Voor een ontwikkelaar die een Mac-app bouwt, is de nuttige vraag welke informatie de interactie moet opleveren. Een tegel met het label Cirkel toevoegen drukt een opdracht uit. Op een leeg tekenoppervlak klikken om daar een cirkel te plaatsen, drukt een opdracht plus een locatie uit. Die extra invoer verandert welke implementatie geschikt is.

De volgende besliswijzer past Apples gedocumenteerde onderscheid toe op een gewone vormeditor voor desktop. Dit zijn implementatieaanbevelingen die zijn afgeleid van de API's, geen regels voor elke mogelijke interface.

Gewenste interactie van de gebruiker Uitgangspunt Reden
Klikken op een aangepaste tegel met het label Cirkel toevoegen Button met een aangepast label Het oppervlak start een benoemde actie.
Die tegel een kenmerkende achtergrond en ingedrukte weergave geven ButtonStyle Hiermee pas je de vormgeving aan en behoud je het standaardknopgedrag.
Op een canvas klikken om op dat punt een cirkel te plaatsen SpatialTapGesture Het resultaat bevat de interactielocatie.
Twee klikken herkennen op een inhoudsweergave zonder coördinaten nodig te hebben onTapGesture(count: 2) De modifier accepteert een vereist aantal klikken.
Specifieke muisknoppen instellen in een AppKit-interactie NSClickGestureRecognizer Deze biedt configuratie voor muisknoppen en het aantal klikken.

Apple documenteert een knop als een actie gekoppeld aan een label en maakt onderscheid tussen aanpassing van de vormgeving via ButtonStyle en aanpassing van de interactie via PrimitiveButtonStyle. De API's voor gebaren documenteren afzonderlijk aantallen klikken en ruimtelijke resultaten. Button, onTapGesture(count:perform:), SpatialTapGesture, NSClickGestureRecognizer

Een aangepaste actietegel begint daarom als knop. Kies een gebaar wanneer je de extra invoer- of herkenningsvereiste kunt benoemen die het oppervlak nodig heeft.

Hoe bouw je een aangepaste knop zonder het gedrag opnieuw te implementeren?

Zet de opdracht in de actieclosure van de knop en de vormgeving in het label. Gebruik ButtonStyle voor de presentatie. Apple documenteert dat dit protocol het standaardinteractiegedrag van het platform behoudt; PrimitiveButtonStyle is het afzonderlijke uitbreidingspunt voor wie het interactiegedrag bewust wil veranderen. ButtonStyle

Het onderstaande voorbeeld bevat twee oppervlakken naast elkaar: een aangepaste knop Cirkel toevoegen en een rechthoekig plaatsingscanvas. Beide roepen dezelfde mutatiefunctie aan. De knop kiest een voorspelbare positie; het canvas levert een lokaal punt. Zo wordt het verschil zichtbaar zonder bestanden, externe inhoud of een andere app toe te voegen.

Maak een macOS-appproject met SwiftUI en stel het implementatiedoel in op macOS 13 of hoger, de gedocumenteerde minimumversie voor SpatialTapGesture. Vervang de gegenereerde app- en content-viewdefinities door het eerste bestand en voeg het tweede bestand aan hetzelfde target toe. Behoud precies één @main-appdeclaratie. De code is nagekeken, maar niet gecompileerd of uitgevoerd in een macOS SDK-omgeving. SpatialTapGesture

De volledige applicatie bestaat uit deze twee bestanden. Eerst ShapeSurfaceApp.swift:

import SwiftUI
import Foundation

@main
struct ShapeSurfaceApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeSurfaceView()
        }
    }
}

private struct PlacedCircle: Identifiable {
    let id = UUID()
    let center: CGPoint
}

struct ShapeSurfaceView: View {
    @State private var circles: [PlacedCircle] = []
    private let limit = 6

    private var isFull: Bool {
        circles.count >= limit
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Button {
                let x = 35 + CGFloat(circles.count) * 50
                addCircle(at: CGPoint(x: x, y: 100))
            } label: {
                HStack(spacing: 12) {
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 24, height: 24)
                    Text("Add circle")
                }
                .frame(width: 280, height: 64)
                .contentShape(RoundedRectangle(cornerRadius: 12))
            }
            .buttonStyle(SurfaceButtonStyle())
            .keyboardShortcut("a", modifiers: [.command, .shift])
            .disabled(isFull)
            .opacity(isFull ? 0.4 : 1)

            Text("Click the canvas to place a circle. Maximum: 6.")

            ZStack(alignment: .topLeading) {
                Color.gray.opacity(0.1)

                ForEach(circles) { circle in
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 24, height: 24)
                        .position(circle.center)
                }
            }
            .frame(width: 320, height: 200)
            .contentShape(Rectangle())
            .gesture(
                SpatialTapGesture()
                    .onEnded { value in
                        addCircle(at: value.location)
                    }
            )
            .disabled(isFull)
            .border(Color.gray)
            .accessibilityElement(children: .ignore)
            .accessibilityLabel("Circle canvas")
            .accessibilityValue("\(circles.count) of \(limit) circles")

            Text(isFull
                 ? "Six circles placed. Clear to start again."
                 : "\(circles.count) of \(limit) circles placed")

            Button("Clear circles") {
                circles.removeAll()
            }
            .disabled(circles.isEmpty)
        }
        .padding(24)
    }

    private func addCircle(at point: CGPoint) {
        guard !isFull else { return }

        // Keep each 24-point circle inside the 320-by-200 canvas.
        let center = CGPoint(
            x: min(max(point.x, 12), 308),
            y: min(max(point.y, 12), 188)
        )
        circles.append(PlacedCircle(center: center))
    }
}

Daarna SurfaceButtonStyle.swift:

import SwiftUI

struct SurfaceButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .background(
                RoundedRectangle(cornerRadius: 12)
                    .fill(Color.blue.opacity(
                        configuration.isPressed ? 0.25 : 0.1
                    ))
            )
    }
}

De stijl leest configuration.isPressed om feedback weer te geven; de stijl roept de actie zelf nooit aan. Apple definieert die eigenschap als de huidige ingedrukte status van de knop. Zo blijven weergave en uitvoering van acties gescheiden. isPressed

De limiet van zes cirkels is een illustratieve applicatieregel. Beide invoerpaden gebruiken addCircle(at:), dat de capaciteit controleert voordat het een cirkel toevoegt. De uitgeschakelde weergave maakt de limiet duidelijk, terwijl de gedeelde controle de mutatieregel consistent houdt. Apple documenteert disabled(_:) als een instelling die bepaalt hoe interacties met een weergave werken, waaronder klikken en gebaren. disabled(_:)

Waarom gebruikt het canvas een ruimtelijk klikgebaar?

Het canvas heeft het punt nodig waarop de gebruiker heeft geklikt. De eenvoudige overload van onTapGesture(count:perform:) levert een actie zonder locatieparameter. SpatialTapGesture rapporteert expliciet een locatie en de initializer stelt standaard één interactie in de lokale coördinatenruimte in. onTapGesture(count:perform:), SpatialTapGesture, init(count:coordinateSpace:)

In dit programma hoort het gebaar bij het canvas met vaste afmetingen, waarvan de lokale coördinaten de positie van de cirkels bepalen. De plaatsingsfunctie begrenst elk middelpunt zodat er een marge van 12 punten overblijft, gelijk aan de straal van de cirkel. Een klik bij de rand plaatst de volledige cirkel dus binnen het canvas. Cirkels mogen elkaar overlappen en de telling blijft zichtbaar, ook als vormen elkaar overlappen.

De knop en het canvas hebben ook bewust verschillend toetsenbordgedrag. De sneltoets van de knop voert dezelfde actie uit als het activeren van die knop: een cirkel toevoegen op de volgende positie. De sneltoets maakt geen muislocatie aan. Apple documenteert dat sneltoetsen de primaire actie van een bedieningselement uitvoeren en merkt op dat bij dubbele sneltoetstoewijzingen de eerste gevonden overeenkomende bediening wordt gekozen. Wijs deze sneltoets in het voorbeeldvenster één keer toe. keyboardShortcut(_:modifiers:)

Het toegankelijkheidslabel en de telling van het canvas beschrijven de inhoud; ze bieden geen toetsenbordworkflow om willekeurige coördinaten in te voeren. De knop biedt een manier om zonder aanwijzer een cirkel op een vooraf bepaalde positie toe te voegen. Als exacte plaatsing ook via toetsenbordbediening mogelijk moet zijn, voeg dan een expliciete interface voor positie-invoer toe in plaats van aan te nemen dat een gebaar of beschrijvend label daarin voorziet.

Hoe laat je hit testing overeenkomen met het aangepaste oppervlak?

Bepaal het bedoelde interactiegebied nadat je de afmetingen van het label hebt vastgesteld. In het voorbeeld heeft het knoplabel een frame van 280 bij 64, gevolgd door een afgeronde rechthoekige contentShape. Het canvas krijgt een frame van 320 bij 200 en definieert vervolgens een rechthoekige inhoudsvorm. Apple definieert contentShape(_:eoFill:) als de vorm die voor hit testing wordt gebruikt. contentShape(_:eoFill:)

Zo krijgen de zichtbare oppervlakken expliciete interactiegrenzen. Als je bij een herontwerp het frame buiten het knoplabel plaatst, controleer dan het resultaat opnieuw: in het voorbeeld horen de afmetingen en de inhoudsvorm samen binnen het label. Houd tekenen, lay-out en interactiegeometrie bewust op elkaar afgestemd in plaats van een niet-reagerende marge te repareren met een extra actiehandler.

Controleer bij een oppervlak dat niet meer reageert de volgende punten in deze volgorde:

  1. Controleer de geometrie. Klik op de tekst, afbeelding en lege binnenruimte van het label. Controleer of de inhoudsvorm de bedoelde grenzen beschrijft.
  2. Controleer uitgeschakelde bovenliggende weergaven. Een uitgeschakelde container schakelt een ingeschakeld onderliggend element ook uit, dus een lokale .disabled(false) herstelt de interactie mogelijk niet. disabled(_:)
  3. Controleer decoratieve overlays. Je kunt een weergave uitsluiten van hit testing met .allowsHitTesting(false) als die puur visueel is. Pas dit toe op de decoratie, niet op het interactieve oppervlak. allowsHitTesting(_:)
  4. Controleer concurrerende handlers. Bepaal welke weergave de actie beheert voordat je de prioriteit van gebaren verandert.

Hoe voorkom je concurrerende handlers en fouten in eigenaarschap?

Gebruik één handler voor elk bedoeld activeringspad. In het voorbeeld beheert de aangepaste knop de opdracht en beheert het canvas het plaatsen op basis van een locatie. Geen van beide staat binnen een bovenliggende weergave met nog een plaatsingsgebaar. Door deze structuur is het beoogde effect van één klik eenvoudig te controleren.

Voeg geen tweede tikhandler aan de knop toe om dezelfde mutatiefunctie aan te roepen. Voeg ook geen gebaar toe aan een bovenliggende weergave rond meerdere bedieningselementen zonder te bepalen of interacties van ouder en kind allebei moeten plaatsvinden. Apples voorbeeld van simultaneousGesture laat zien dat handlers van ouder en kind allebei kunnen worden uitgevoerd. highPriorityGesture geeft het gekoppelde gebaar juist een hogere prioriteit. Deze modifiers leggen interactiebeleid vast; ze zijn geen onderling uitwisselbare reparaties. simultaneousGesture(_:including:), highPriorityGesture(_:including:)

Bewaar applicatiestatus bij de weergave die er eigenaar van is. Hier bewaart ShapeSurfaceView de voorbeeldcirkels en geeft de stijl de aangeleverde labelweergave alleen weer. Apple raadt aan om status privé te houden in de hoogste weergave die er toegang toe nodig heeft en waar nodig waarden of bindings aan onderliggende weergaven door te geven. State

Als een latere AppKit-adapter een callback opslaat, controleer dan de vastleggingen afzonderlijk. Swift-closures kunnen omringende waarden vastleggen; een klasse die een closure bewaart die diezelfde instantie vastlegt, kan een sterke referentiecyclus veroorzaken. Kies een capture list op basis van de werkelijke levensduur van het object. Een knop vervangen door een gebaar lost geen problemen met callback-eigenaarschap op. Closures

Wanneer is AppKit de juiste volgende stap?

Overweeg AppKit wanneer je eigen oppervlak de gedocumenteerde muisspecifieke configuratie nodig heeft. NSClickGestureRecognizer ondersteunt een vereist aantal klikken en een mask voor muisknoppen. Apple specificeert dat bit nul de primaire knop aangeeft en bit één de secundaire knop; herkenning vereist het ingestelde aantal klikken zonder slepen. Ook worden gebeurtenisvertragingen automatisch ingeschakeld voor de ingestelde muisknoppen. NSClickGestureRecognizer, buttonMask

Kies de integratieroute op basis van je implementatiedoel en het onderdeel dat je beheert:

  • Op macOS 26 of hoger integreert NSGestureRecognizerRepresentable een AppKit-herkenner rechtstreeks in een SwiftUI-hiërarchie via .gesture(...). SwiftUI installeert het actiedoel; implementeer de actiehandler van de representable en gebruik de converter in de context voor coördinatenconversie. Apple waarschuwt dat de bijbehorende weergave van de herkenner een andere geometrie kan hebben dan de SwiftUI-weergave. NSGestureRecognizerRepresentable
  • Voor een AppKit-weergave die in SwiftUI wordt gehost beheert NSViewRepresentable het aanmaken, bijwerken en afbreken van je weergave. Gebruik de coordinator voor callbacks en laat SwiftUI de lay-out van de weergegeven weergave beheren. Deze API is beschikbaar vanaf macOS 10.15. NSViewRepresentable

Behoud de grens van de gebeurtenissen wanneer je een van beide adapters toevoegt. Apple documenteert dat een herkenner werkt op de weergave waaraan deze is gekoppeld en de onderliggende weergaven daarvan, relevante gebeurtenissen ontvangt voordat de weergave ze krijgt en de bezorging ervan kan vertragen. De documentatie vermeldt ook expliciet dat het niet wordt ondersteund om gebeurtenisafhandeling over te dragen aan andere mechanismen, zoals slepen en neerzetten of pop-upmenu's. Ontwerp zulke interacties met de daarvoor ondersteunde mechanismen in plaats van een klikherkenner als universele gebeurtenisdoorgever te gebruiken. NSGestureRecognizer

Wat moet je controleren in de voltooide Mac-interactie?

Bouw het voorbeeld in Xcode en controleer het waarneembare gedrag voordat je het aanpast aan een grotere weergavehiërarchie:

  • Klik op de tekst en de lege binnenruimte van de aangepaste knop. Elke activering moet één cirkel toevoegen en de telling één keer bijwerken.
  • Druk in het actieve venster op Command–Shift–A. Dit moet de actie van de knop uitvoeren die de volgende positie gebruikt.
  • Klik op verschillende plaatsen in het canvas, ook dicht bij de randen. Cirkels moeten de lokale coördinaten volgen en de marge behouden die in de code is vastgelegd.
  • Bereik zes cirkels. Beide plaatsingspaden moeten stoppen met toevoegen, terwijl Cirkels wissen beschikbaar blijft.
  • Wis het canvas. Plaatsen moet weer mogelijk zijn.
  • Bekijk de knop met VoiceOver en test de toetsenbordnavigatie met de huidige instellingen van je Mac. Controleer of de actie na het toepassen van de stijl begrijpelijk blijft.

Begin voor een aangepast Mac-actievlak bij de betekenis van de interactie: een benoemde opdracht hoort in een Button, visuele aanpassingen horen in het label en de stijl, en plaatsing op basis van een locatie hoort bij het canvasgebaar. Verbind die paden via één functie die de status wijzigt en introduceer AppKit pas wanneer de extra configuratie aan een concrete vereiste voldoet.

Bronnen