如果拖曳需要你自行實作明確的放置規則,請選擇 SwiftUI Canvas。如果圖形應該互相推擠、反彈,或透過物理模擬持續移動,請選擇 SpriteKit。先決定拖曳的圖形碰到另一個圖形時會發生什麼事。
本指南探討一項任務:為包含合成、記憶體內圓形或矩形的 Mac app 選擇互動架構,讓使用者在 app 自有的工作區內拖曳這些圖形。選擇取決於碰撞行為、輸入事件的路由方式,以及由哪個元件控制各圖形的位置。
兩個圖形相遇時應該發生什麼事?
「碰撞反應」可以描述幾種不同需求。標示重疊、拒絕無效位置,以及讓兩個物件反彈,需要不同的機制。Apple 明確區分 SpriteKit 的接觸與碰撞:接觸會回報物體相觸,碰撞則會計算物理反應並施加衝量。碰撞與接觸簡介
| 預期行為 | 實用的起始做法 | 你仍須負責的部分 |
|---|---|---|
| 指標可自由放置圖形,同時標示重疊情況 | 使用自訂幾何測試的 Canvas | 命中測試、重疊偵測和視覺回饋 |
| 圖形遇到第一個障礙物時停止 | 使用自訂移動限制的 Canvas | 測試移動路徑並決定可接受的位置 |
| 推動相鄰圖形,並允許它們移動至穩定位置或反彈 | SpriteKit | 物理設定和拖曳控制器 |
| 每個項目都需要一般的 SwiftUI 控制項和手勢 | 個別的 SwiftUI 檢視 | 應用程式專屬的重疊或移動規則 |
這些是根據已記載功能所提出的架構建議,並非 Apple 訂定的效能門檻。圖形數量少不代表一定該選 Canvas,數量多也不代表一定需要 SpriteKit。先決定要實現的行為,再用具代表性的項目測量實作效能。
還有一項值得考慮的互動限制:精確追蹤指標位置與嚴格禁止重疊可能互相衝突。如果指標移入已被佔用的位置,app 必須決定允許項目重疊、讓項目停在指標後方,或移動其他項目。選擇繪圖方式前,先明確定義這項行為。
Canvas 提供什麼?你必須自行建置什麼?
Canvas 透過圖形內容提供即時繪圖。它繪製的圓形、路徑和符號不會成為可獨立互動的元素。Apple 也指出,Canvas 不會為個別繪製元素提供無障礙支援,包括以檢視形式提供的符號。因此,附加到 Canvas 的手勢需要應用程式邏輯判斷使用者選取了哪個項目。Canvas
若是具有明確放置規則的編輯器,請將圖形保存在模型中,並把繪圖視為模型的呈現結果。每個項目可以包含識別碼、中心點、尺寸和繪圖順序。Swift 的指南建議一般資料預設使用結構,需要共享識別或架構繼承時才使用類別。值型別的項目模型和 SpriteKit 場景子類別,各有不同用途。選擇結構或類別
Canvas 拖曳可以依照以下步驟進行:
- 手勢開始時,依繪圖順序的反向順序,測試起始指標位置是否落在項目內,並選取第一個符合的項目。
- 整個手勢期間保留選取項目的識別碼及其原始中心點。
- 每次根據該原始中心點加上手勢的總位移,計算預定的中心點。
- 套用工作區邊界和碰撞規則,得出可接受的中心點。
- 更新模型,讓 Canvas 依模型重新繪製。
- 手勢結束時清除選取狀態,並定義互動中斷時的清理方式。
區分總位移和增量移動很重要。SwiftUI 將 translation 定義為從手勢起點算起的移動距離。若每次都把這個總位移加到已更新的中心點上,移動距離就會累積過多。每次都應使用保留的起始中心點來計算。SwiftUI 會透過手勢值提供 startLocation、location 和 translation,並允許你指定手勢的座標空間。DragGesture.Value、DragGesture
拖曳期間應維持選取狀態。如果每次指標更新都重新執行初始命中測試,指標越過其他圖形時,選取項目可能會改變。同樣地,保留指標與中心點之間的初始偏移,可避免圖形突然跳動到中心點正好位於指標下方。這些都是應用程式設計規則,不是 Canvas 會自動提供的行為。
實例:重疊偵測與阻擋移動
假設有兩個圓形,各自半徑為 20 點。圓形 A 起始於 (100, 100),靜止的圓形 B 位於 (200, 100)。使用者要求將 A 的中心移至 (190, 100)。兩者中心點的距離會是 10 點,半徑總和則為 40 點,因此這個候選位置會造成重疊。
若要顯示重疊提示,就接受這個位置並變更繪圖結果。若要阻擋水平拖曳,第一次接觸的位置是 (160, 100):此時兩個中心點相距 40 點。這項計算假設 B 保持固定、兩個圓形的尺寸不變,而且 A 沿著水平路徑移動。
現在假設某次指標更新要求移至 (260, 100)。只測試終點會發現沒有重疊,即使移動路徑穿過了 B 也是如此。阻擋拖曳的實作需要掃掠交會測試,或其他明確的方法來檢查中間路徑。只拒絕互相重疊的終點,並不能實現「遇到第一個障礙物時停止」。
這個例子說明了為何必須明確定義碰撞反應。若只要顯示重疊,幾項終點幾何測試可能就足以完成功能。若要支援沿障礙物滑動、多個物體同時接觸,或移動相鄰物件,自訂求解器就會大幅複雜化。Canvas 仍可用來繪圖,但這些額外行為都由你的應用程式負責。
何時 SpriteKit 會更適合?
當碰撞反應應該由模擬自然產生時,SpriteKit 會很有吸引力。將 SKPhysicsBody 附加到節點後,就能進行涉及重力、摩擦力和碰撞的計算;模擬接著會更新節點的位置和方向。它支援用於物件的體積物體,以及用於邊界的邊緣物體。SKPhysicsBody
第一個原型可使用兩個分開的圓形和一個矩形工作區邊界。為圓形設定相符的圓形物體,在桌面式工作區中停用重力,並決定旋轉是否能改善互動體驗。應明確設定摩擦力、恢復係數和阻尼,而不要依賴預設值。這些屬性會決定物理行為的不同面向。SKPhysicsBody
SwiftUI 可透過 SpriteView 直接承載場景;Apple 將其記載為能呈現 SpriteKit 場景的 SwiftUI 檢視。其文件所列的 Mac 最低支援版本為 macOS 11;Canvas 則從 macOS 12 開始支援。因此,部署目標也應納入考量,儘管互動行為仍是主要判斷依據。SpriteView、Canvas
將碰撞反應與通知分開設定
為圖形和邊界指定類別,接著明確設定兩種互動遮罩:
categoryBitMask用來識別物體所屬的類別。collisionBitMask用來選擇可能對該物體造成物理作用的類別。contactTestBitMask用來要求針對所選類別傳送接觸通知。
各物體會各自評估碰撞遮罩。若要讓兩個動態圖形互相產生一般的物理反應,必須將兩個物體都設為能對另一方的類別產生反應。接觸回報的規則則不同:只要任一方向的類別與接觸遮罩比對成功,就可能產生通知。單獨啟用通知並不會設定實際的物理阻擋。collisionBitMask、contactTestBitMask
若物體會互相產生物理反應,卻沒有收到回呼,請檢查接觸遮罩和物理世界的接觸委派物件。若收到回呼,但圖形彼此穿過,請檢查碰撞遮罩和移動控制器。這些檢查能將通知設定問題與實際移動問題區分開來。
使用者拖曳時由誰負責位置?
SpriteKit 的拖曳需要明確的控制器。物理引擎本身不會自動追隨指標。請根據前面定義的行為,在直接放置和模擬驅動移動之間做出選擇。
直接放置時,由應用程式程式碼指定被拖曳節點的位置。如果指標必須精確放置項目,這種做法很適合,但它本身不會在連續位置之間建立經物理求解的路徑。關閉 isDynamic 會讓物體忽略力和衝量,但不會建立拖曳求解器。如果你暫時變更這項屬性,請定義何時恢復,以及放開時要保留還是清除動量。isDynamic
使用模擬驅動拖曳時,請將指標位置保留為目標,並使用作用力或彈簧式控制器,引導動態物體移向該目標。移動物體時,模擬就能解析接觸情況。這種做法的取捨是:遇到其他物件阻擋時,物體會刻意延遲移動,或無法完全貼合指標。SpriteKit 支援施加作用力,而 SKPhysicsJointSpring 能以彈簧行為連接兩個物理物體。這些都是基礎工具,仍須依應用程式需求調校。SKPhysicsBody、SKPhysicsJointSpring
若預期物理引擎會獨立控制位置,就避免持續指派指標位置。SpriteKit 的文件指出,每影格執行順序會分別進行 app 更新、動作、物理模擬和後續回呼。實用的位置管理原則是:由輸入更新拖曳目標、由場景邏輯套用控制器,再由物理模擬產生物體的最終位置。回應影格週期事件
接觸回呼也有文件明確說明的限制。Apple 指出,回呼會在物理模擬步驟期間執行,此時不支援修改物理世界,變更模擬中的物體也會導致未定義行為。請在回呼中記錄待處理工作,並依照 Apple 的旗標與更新模式,在後續更新中套用物理變更。SKPhysicsContactDelegate
macOS 輸入應如何跨越架構邊界?
為工作區選定一個輸入事件處理者。Canvas 實作可將 SwiftUI DragGesture 附加到互動表面。自訂 AppKit 表面則可處理回應者事件;mouseDragged(with:) 會在按住滑鼠左鍵移動時接收事件。避免讓疊加的 SwiftUI 手勢和 AppKit 處理常式分別移動同一個選取項目。DragGesture、mouseDragged(with:)
若要在 AppKit 與 SpriteKit 之間建立橋接,請在每個邊界進行座標轉換。NSEvent.locationInWindow 通常以關聯視窗的基底座標系表示。先將該點轉換為承載視圖的座標,再將視圖座標轉換為場景座標。Apple 提供 convertPoint(fromView:) 執行後一項轉換,並要求先呈現場景才能呼叫。節點的位置以其父節點的座標系表示,因此巢狀節點可能需要再做一次轉換。locationInWindow、convertPoint(fromView:)、position
一般的 SwiftUI 承載方式可使用 SpriteView。若你需要自訂 AppKit 檢視,NSViewRepresentable 可負責建立、更新、協調和拆除。請讓 SwiftUI 負責所承載檢視的版面配置:Apple 明確警告不要自行指定由其管理的 frame 或 bounds。NSViewRepresentable
請由穩定的擁有者保留場景,不要每次 SwiftUI 重新計算檢視內容時都建立新場景。Canvas 的項目模型也應以相同方式保留。SwiftUI 的模型文件說明了受管理的狀態如何在檢視重建期間保留實例;其 Observation 方法需要 macOS 14 或更新版本。請明確傳遞場景命令和模型快照,避免兩份可變副本同時爭相決定位置。管理 app 中的模型資料
選定做法之前應測試什麼?
先用小型合成工作區測試棘手的互動,再加入視覺修飾:
- 抓取圖形邊緣附近的位置,確認初始偏移得以保留。
- 緩慢拖向另一個項目,再以一次大幅指標移動越過該項目。
- 將圖形拖向兩個相鄰圖形形成的角落。
- 在接觸另一個項目時放開,並確認是否符合預期的穩定行為。
- 調整工作區大小,確認命中測試、邊界和座標轉換正常。
- 重建周圍的 SwiftUI 內容,確認項目位置仍然保留。
- 中斷拖曳,確認選取狀態和任何暫時的物理設定都已清除。
在 SpriteKit 中,檢查物體幾何是否符合預期碰撞區域時,請啟用 SKView.showsPhysics。對於快速移動的小型物體,可評估 usesPreciseCollisionDetection;Apple 記載其計算成本較高,但能偵測一般逐影格檢查可能漏掉的碰撞。它不能取代有一致邏輯的拖曳控制器。SKView、usesPreciseCollisionDetection
若放置規則清楚、範圍有限,而且容易直接表達,請選擇 Canvas。若圖形之間的物理互動是任務的一部分,請選擇 SpriteKit。無論採用哪種實作,可靠的桌面拖曳都始於選取單一項目、明確轉換座標,以及由單一元件負責每次位置更新。
資料來源
- Canvas — Apple
- DragGesture — Apple
- DragGesture.Value — Apple
- 選擇結構或類別 — Apple
- 碰撞與接觸簡介 — Apple
- SKPhysicsBody — Apple
- SpriteView — Apple
- collisionBitMask — Apple
- contactTestBitMask — Apple
- isDynamic — Apple
- SKPhysicsJointSpring — Apple
- 回應影格週期事件 — Apple
- SKPhysicsContactDelegate — Apple
- mouseDragged(with:) — Apple
- locationInWindow — Apple
- convertPoint(fromView:) — Apple
- position — Apple
- NSViewRepresentable — Apple
- 管理 app 中的模型資料 — Apple
- SKView — Apple
- usesPreciseCollisionDetection — Apple