自訂 Mac 介面執行具名動作時,請使用 SwiftUI Button。若互動需要手勢資訊,例如畫布上的位置,請使用點擊手勢。本指南會建立一個小型圖形編輯器,示範兩者各自適用的情境。
Apple 明確建議,只要元素的功能如同按鈕,就應使用按鈕。按鈕可以使用任何檢視作為標籤,因此自訂外觀不必以替換互動行為為代價。SwiftUI 提供標準按鈕行為,包括輔助使用標籤與提示。手勢
介面需要哪種互動?
對於開發 Mac App 的開發者來說,關鍵問題是互動必須提供哪些資訊。標示為 Add circle 的圖塊代表一項命令。在空白繪圖介面上點擊以放置圓形,則代表一項命令加上一個位置。這項額外輸入會改變適合採用的實作方式。
以下決策參考依據 Apple 文件所述的區別,套用到一般桌面圖形編輯器。這些是根據 API 得出的實作建議,並非適用於所有介面的規則。
| 讀者想要的互動 | 起始做法 | 原因 |
|---|---|---|
點擊標示為 Add circle 的自訂圖塊 |
使用自訂標籤的 Button |
介面會啟動一項具名動作。 |
| 為圖塊設定獨特背景與按下時的外觀 | ButtonStyle |
可自訂外觀,同時保留標準按鈕互動。 |
| 點擊畫布並在該位置放置圓形 | SpatialTapGesture |
結果包含互動位置。 |
| 在內容檢視上辨識兩次點擊,但不需要座標 | onTapGesture(count: 2) |
此修飾器可接受指定的點擊次數。 |
| 在 AppKit 互動中設定特定滑鼠按鈕 | NSClickGestureRecognizer |
可設定滑鼠按鈕與點擊次數。 |
Apple 將按鈕說明為搭配標籤的動作,並區分透過 ButtonStyle 自訂外觀,以及透過 PrimitiveButtonStyle 自訂互動。其手勢 API 則分別說明點擊次數與空間位置結果。Button、onTapGesture(count:perform:)、SpatialTapGesture、NSClickGestureRecognizer
因此,自訂動作圖塊應從按鈕開始。只有在能指出介面需要的額外輸入或辨識需求時,才使用手勢。
如何自訂按鈕,而不必重建其行為?
將命令放在按鈕的動作閉包中,並將圖像放在其標籤中。使用 ButtonStyle 控制呈現方式。Apple 文件指出,這個協定會保留平台的標準互動行為;若要刻意改變互動行為,則有獨立的擴充點 PrimitiveButtonStyle。ButtonStyle
以下範例包含兩個並列介面:自訂的 Add circle 按鈕,以及矩形放置畫布。兩者都會呼叫同一個狀態變更函式。按鈕會選擇可預期的插槽;畫布則提供區域座標點。如此一來,不必加入檔案、外部內容或另一個 App,就能清楚看出兩者的差別。
使用 SwiftUI 建立 macOS App 專案,並將部署目標設為 macOS 13 或更新版本,這是 SpatialTapGesture 文件記載的最低版本。將產生的 App 與內容檢視定義替換為第一個檔案,並將第二個檔案加入相同目標。保留且只保留一個 @main App 宣告。程式碼已檢視,但尚未在 macOS SDK 環境中編譯或執行。SpatialTapGesture
完整應用程式由以下兩個檔案組成。首先是 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))
}
}
接著是 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
))
)
}
}
此樣式會讀取 configuration.isPressed 來繪製回饋;它不會自行呼叫動作。Apple 將此屬性定義為按鈕目前的按下狀態。如此可將繪製與執行動作分開處理。isPressed
六個圓形的上限是範例中的應用程式規則。兩種操作方式都會使用 addCircle(at:),在新增前檢查容量。停用外觀會傳達上限,而共用的保護條件則確保狀態變更規則一致。Apple 文件指出,disabled(_:) 用來控制檢視的互動,包括點擊與手勢。disabled(_:)
為什麼畫布使用空間點擊手勢?
畫布需要知道使用者點擊的位置。基本的 onTapGesture(count:perform:) 多載會提供一個不含位置參數的動作。SpatialTapGesture 會明確回報位置,其初始設定預設為在區域座標空間中辨識一次互動。onTapGesture(count:perform:)、SpatialTapGesture、init(count:coordinateSpace:)
在此程式中,手勢附加於固定尺寸的畫布,其區域座標用來定位圓形。放置函式會限制每個圓心的位置,保留 12 點邊界,與圓形半徑相同。因此,即使點擊靠近邊緣,完整的圓形仍會位於畫布內。圓形可以重疊;即使形狀重疊,數量仍會顯示。
按鈕與畫布的鍵盤行為也刻意不同。按鈕的快速鍵會執行與啟用該按鈕相同的動作,在下一個插槽新增圓形。它不會產生指標位置。Apple 文件說明,鍵盤快速鍵會呼叫控制項的主要動作,並指出若快速鍵指派重複,會由第一個找到的相符控制項處理。在範例視窗中只指派一次此快速鍵。keyboardShortcut(_:modifiers:)
畫布的輔助使用標籤與數量會描述其內容,但不會提供任意座標放置的鍵盤操作流程。按鈕則提供非指標操作方式,可在預先定義的插槽新增圓形。如果也必須透過鍵盤控制精確放置,請加入明確的位置編輯介面,不要假設手勢或描述性標籤已提供這項功能。
如何讓點擊測試符合自訂介面?
先確定標籤的尺寸,再定義預期的互動區域。在範例中,按鈕標籤的框架為 280 x 64,後接一個圓角矩形 contentShape。畫布先設定 320 x 200 的框架,再宣告矩形內容形狀。Apple 將 contentShape(_:eoFill:) 定義為點擊測試所使用的形狀。contentShape(_:eoFill:)
如此可為可見介面明確設定互動邊界。若重新設計時將框架移到按鈕標籤之外,請再次檢查結果:範例中的尺寸設定與內容形狀必須一併留在標籤內。請明確規劃繪圖、版面配置與互動幾何,不要靠新增動作處理常式來補救沒有反應的邊界區域。
介面停止回應時,請依序檢查以下條件:
- 檢查幾何範圍。 點擊標籤的文字、圖像與空白內部區域。確認內容形狀涵蓋預期邊界。
- 檢查已停用的上層檢視。 已停用的容器會覆蓋已啟用的子檢視,因此區域性的
.disabled(false)可能無法恢復互動。disabled(_:) - 檢查裝飾性覆疊。 如果某個檢視純粹用於視覺裝飾,可透過
.allowsHitTesting(false)將其排除於點擊測試之外。將此設定套用於裝飾元素,而非互動介面。allowsHitTesting(_:) - 檢查競爭中的處理常式。 在變更手勢優先順序之前,先確認哪個檢視擁有此動作。
如何避免處理常式互相競爭及所有權錯誤?
每種預期的啟用方式只使用一個處理常式。在範例中,自訂按鈕負責其命令,畫布則負責依位置放置。兩者都不會放在另一個也附有放置手勢的父檢視中。這種結構讓人容易檢查單次點擊的預期效果。
不要為了呼叫相同的狀態變更函式,而在按鈕上再附加第二個點擊處理常式。同樣地,在多個控制項周圍加上父層手勢前,應先決定父層與子層互動是否都應執行。Apple 的 simultaneousGesture 範例展示了父層與子層的處理常式可以同時執行。highPriorityGesture 則會讓附加的手勢取得較高優先順序。這些修飾器表達的是互動政策,不能互相替代來修補問題。simultaneousGesture(_:including:)、highPriorityGesture(_:including:)
將應用程式狀態放在負責管理它的檢視中。此處由 ShapeSurfaceView 儲存範例圓形,樣式則只負責繪製傳入的標籤。Apple 建議在需要存取該狀態的最高層檢視中使用私有狀態,並視需要將值或繫結傳給子檢視。State
若後續的 AppKit 介接器儲存了回呼,請另外檢查其擷取關係。Swift 閉包可以擷取周圍的值;若某個類別儲存的閉包擷取了同一個實例,可能會形成強參照循環。請依實際物件生命週期選擇擷取清單。將按鈕改成手勢並不能解決回呼的所有權問題。Closures
何時該改用 AppKit?
當你自己的介面需要文件記載的滑鼠專屬設定時,可考慮使用 AppKit。NSClickGestureRecognizer 支援設定必要點擊次數與滑鼠按鈕遮罩。Apple 指定位元 0 代表主要按鈕,位元 1 代表次要按鈕;辨識時必須達到設定的點擊次數,且不可拖曳。它也會自動為已設定的滑鼠按鈕啟用事件延遲。NSClickGestureRecognizer、buttonMask
請依照部署目標與你負責的元件選擇介接方式:
- macOS 26 或更新版本:
NSGestureRecognizerRepresentable可透過.gesture(...),將 AppKit 辨識器直接整合至 SwiftUI 階層。SwiftUI 會安裝動作目標;請實作此可表示型別的動作處理常式,並使用其內容轉換器進行座標轉換。Apple 提醒,辨識器所關聯的檢視,其幾何範圍可能與 SwiftUI 檢視不同。NSGestureRecognizerRepresentable - 在 SwiftUI 中託管 AppKit 檢視:
NSViewRepresentable負責建立、更新與拆除你的檢視。使用其協調器處理回呼,並讓 SwiftUI 控制所表示檢視的版面配置。此 API 自 macOS 10.15 起提供。NSViewRepresentable
加入任一介接器時,請保留事件邊界。AppKit 文件指出,辨識器會在所附加的檢視及其子檢視上運作,在檢視之前接收相關事件,並可能延遲事件傳遞。文件也明確指出,它不支援將事件追蹤交給拖放或彈出式選單等其他機制處理。請透過支援的機制設計這些互動,不要把點擊辨識器當作通用事件轉送器。NSGestureRecognizer
完成 Mac 互動後應確認哪些項目?
在 Xcode 中建置範例,並在將其套用至更大的檢視階層前確認可觀察到的行為:
- 點擊自訂按鈕的文字與空白內部區域。每次啟用都應新增一個圓形,且數量只更新一次。
- 在作用中的視窗按下
Command–Shift–A。它應執行按鈕以插槽為基礎的動作。 - 點擊畫布的不同位置,包括邊緣附近。圓形應依據區域座標放置,並保留程式碼中註明的邊界。
- 放置六個圓形。兩種放置方式都應停止新增,而
Clear circles仍可使用。 - 清除畫布後,應可再次放置圓形。
- 使用 VoiceOver 檢查按鈕,並依照 Mac 目前的設定測試鍵盤導覽。確認套用樣式後,按鈕的動作依然清楚易懂。
對於自訂 Mac 動作介面,請從互動意義著手:具名命令應放在 Button 中,視覺自訂應放在標籤與樣式中,而依位置放置應交由畫布手勢處理。讓這些操作路徑共用一個變更狀態的函式;只有在 AppKit 的額外設定能滿足具體需求時,才引入 AppKit。
來源
Button — Apple 開發者文件。
手勢 — Apple 開發者文件。
ButtonStyle — Apple 開發者文件。
isPressed — Apple 開發者文件。
onTapGesture(count:perform:) — Apple 開發者文件。
SpatialTapGesture — Apple 開發者文件。
init(count:coordinateSpace:) — Apple 開發者文件。
contentShape(_:eoFill:) — Apple 開發者文件。
disabled(_:) — Apple 開發者文件。
allowsHitTesting(_:) — Apple 開發者文件。
keyboardShortcut(_:modifiers:) — Apple 開發者文件。
simultaneousGesture(_:including:) — Apple 開發者文件。
highPriorityGesture(_:including:) — Apple 開發者文件。
State — Apple 開發者文件。
Closures — Swift.org,《Swift 程式語言》。
NSClickGestureRecognizer — Apple 開發者文件。
buttonMask — Apple 開發者文件。
NSGestureRecognizer — Apple 開發者文件。
NSGestureRecognizerRepresentable — Apple 開發者文件。
NSViewRepresentable — Apple 開發者文件。