若要透過重疊檢視中的透明孔洞點擊 SwiftUI 控制項,請為上層檢視明確指定一個排除該孔洞的命中測試形狀。本指南會在單一 Mac app 中建立這種互動,使用記憶體中的圓環和位於其下方的按鈕。

具體來說,這是一個小型桌面繪圖介面:點擊橘色圓環會選取圓環;點擊其透明中心則會啟用下方可見的藍色控制項。兩個圖層都屬於同一個 SwiftUI 檢視階層。範例使用繪圖和命中測試共用的幾何,因此不需要影像檔案或像素取樣。

變更事件處理方式前,先選擇命中測試規則

Apple 記錄了兩個相關的 SwiftUI 控制項。allowsHitTesting(_:) 可決定檢視是否參與命中測試。contentShape(_:eoFill:) 則指定命中測試所用的形狀。若裝飾元素永遠不應接收點擊,請使用第一個;若互動式覆蓋層有孔洞,請使用第二個。Apple 關於命中測試參與方式的文件和內容形狀文件分別說明了這兩種用途。

上層需求 實作方式
裝飾元素應忽略所有點擊 對該裝飾元素套用 .allowsHitTesting(false)
圓形應在其輪廓內回應 為互動檢視指定圓形內容形狀
圓環應在透明中心周圍回應 繪圖和命中測試都使用複合形狀與奇偶填充
自有的 AppKit 子樹需要選擇性參與 覆寫其 hitTest(_:) 邊界,並拒絕互動區域以外的點

這些 API 描述參與方式和幾何;它們並未規定會自動測試最終渲染像素的 alpha 值。對於合成形狀,支援的做法是明確描述預期的互動區域。因此,僅有透明外觀並不足以完整定義互動。這項區別是根據文件所述的布林值和形狀介面而來,並非對所有繪圖修飾子的行為保證。

建立完整的 SwiftUI 圓環覆蓋按鈕範例

在 Xcode 中使用 SwiftUI 建立 macOS app,並將部署目標設為 macOS 12 或更新版本。將產生的 app 宣告和初始檢視替換為下方完整程式,只在目標中保留一個 @main 宣告。此範例使用 .contentShape(.interaction, …),其互動類型自 macOS 12 起提供,並可明確控制命中測試。Apple 的互動文件說明了這項行為及其適用版本。

圓環包含兩個同心橢圓子路徑。繪圖和互動都使用奇偶規則,因此內側橢圓會成為孔洞。Apple 將 Shape.path(in:) 記錄為供應矩形內的幾何,並將 FillStyle 記錄為包含奇偶填充在內的渲染設定。形狀幾何和FillStyle說明了其基本規範。

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()
        }
    }
}

下方按鈕仍負責自己的動作。圓環的手勢則會遞增另一個計數器。這兩個尾隨閉包直接表示這兩項動作;Swift 語言指南說明閉包是可以擷取周圍值的功能。《Swift 程式語言:閉包》說明了語法和擷取行為。

灰色外框是裝飾元素,因此只對它停用命中測試。請將此修飾子保留在外框上:若套用到包含互動圖層的容器,容器就會採用錯誤的參與規則。圓環本身必須維持可互動,才能在橘色環帶上回應。

為何填充規則要設定兩次

繪圖呼叫和內容形狀修飾子都設為 eoFill: true。若只在繪圖時設定,雖然定義了可見孔洞,卻沒有明確讓命中測試採用相同規則。Apple 將內容形狀參數與渲染分開記錄,因此此範例需要同時宣告兩者。內容形狀修飾子會定義互動形狀及其繞行規則。

尺寸和轉換也要保持一致。此處圓環的繪圖和內容形狀共用一個 240 點的正方形。若之後移動或縮放整個圓環檢視,請將這些操作一併套用。若只變更較大容器中的繪圖路徑,則要更新互動幾何,讓它描述相同的預期區域。

用三次特定點擊驗證路由

啟用 app 視窗,然後點擊遠離形狀邊界的位置。以下是 240 點正方形內的示意區域座標,以左上角為原點。這些點分別對應中心孔洞、橘色環帶和透明角落,不必依賴抗鋸齒邊緣像素。

點 上層可見幾何 預期計數器變化
(120, 120) 中心孔洞 Below 遞增一次
(210, 120) 橘色環帶 Ring 遞增一次
(10, 10) 圓環外側圓之外 Below 遞增一次

外半徑為 120 點。內側每邊內縮 72 點,因此內半徑為 48 點。三個測試點與中心的距離分別為 0、90 和約 155.6 點。我使用 Python 檢查了這項確定性幾何計算;由於此環境沒有 Swift 工具鏈,雖然檢視了 SwiftUI app,卻無法編譯或執行。

這項測試也有助於診斷問題。如果橘色環帶沒有回應,請檢查圓環的手勢和參與修飾子。如果點擊中心會觸發圓環,請檢查其內容形狀和奇偶參數。如果孔洞中的點擊沒有任何圖層回應,請暫時移除其他覆蓋層,再逐一加回,以找出哪個圖層改變了路由。

若要在自己的程式碼中直接檢查幾何,Path.contains(_:eoFill:) 也支援相同的奇偶選項。傳入以你建立路徑時所用座標系表示的點。如此一來,幾何測試就能提出相同的內外判斷,而不會把螢幕截圖當作互動模型。Apple 關於路徑包含判定的文件說明了這項操作。

僅在自有邊界使用 AppKit 命中測試

若覆蓋層已使用 AppKit 實作,NSView.hitTest(_:) 是文件所述的自訂切入點。它會傳回針對某個點所選取的子檢視,主要用來選擇接收滑鼠按下事件的對象。輸入點使用接收者的父檢視座標,因此與本地幾何比較前要先轉換座標。Apple 的 NSView 命中測試文件說明了這些細節。

對於自有的自訂檢視,覆寫方法可以拒絕互動區域以外的點,並將接受的點交由 super.hitTest(_:) 處理。以下是既有自訂 NSView 子類別的方法片段,不是另一個完整 app。它使用上方定義的 RingShape,並且需要與 import SwiftUI 一起加入 import AppKit。

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)
}

只有在自訂檢視的可見內容和子樹確實遵循該圓環幾何時,才使用此片段。傳回 nil 會拒絕該點對應的子樹;它不會指定目的地,也不會命令 SwiftUI 啟用特定同層檢視。預期的下層能否接收點擊,仍取決於周圍的檢視階層。若有外層檢視仍接受該點,它仍可能阻擋點擊。

在 SwiftUI 中嵌入自有的 AppKit 檢視時,請使用 NSViewRepresentable 建立該檢視並更新其設定。透過 representable 的更新方法同步項目狀態和互動幾何;若要橋接委派或 target-action 訊息,請使用協調器。Apple 也要求開發者讓 SwiftUI 控制被呈現根檢視的 frame 和 bounds。Apple 的 NSViewRepresentable 文件說明了這些生命週期和所有權界線。

保留原本的事件和手勢負責對象

請在命中測試期間做出路由判斷。避免攔截上層點擊,再手動呼叫下層控制項的動作:這會以應用程式命令取代下層控制項原有的互動。同樣地,此範例不需要重新傳送事件、走訪 SwiftUI 的實作檢視,或替換下層的手勢處理器。每個可見的互動圖層都由自身負責行為。

此處的範圍是在 app 檢視階層內進行一般的主按鈕啟用操作。中心點擊成功並不能證明捲動、拖曳、指向或情境選單也會正常運作;若介面加入這些功能,請分別測試。尤其是 Apple 區分了互動內容形狀、拖移預覽形狀和情境選單預覽形狀,後兩者不會改變命中測試。Apple 的內容形狀文件說明了這些不同類型。

對於這項桌面工作,請將圓環路徑維持為可見環帶和可點擊區域的共用定義。這樣一來,透明中心就有了明確意義:上層放棄該點,下方的 SwiftUI 控制項則仍負責透過一般互動系統作出回應。

來源