若要在 Mac app 的兩個窗格之間複製合成形狀,請先使用具型別的 SwiftUI 拖放。若現有的提供者介面或較早版本的 macOS 部署目標要求使用項目提供者,再選擇它們。選擇任一 API 前,先定義負載、接受規則和模型的擁有者。

定義成功放置的意義

此處的讀者任務很明確:讓使用者能將合成圓形或正方形,從調色盤拖曳到同一個 Mac app 內的集合中。目的地會建立新項目;調色盤維持不變。所有資料都存在記憶體中,關閉視窗時集合便會消失。

Apple 建議 Swift 拖放使用 transferable 項目。其文件也支援以 provider 為基礎的修飾子,因此選擇取決於部署需求和你需要整合的介面。拖放 — Apple。

實作檢視畫面前,先寫下傳輸契約:

契約問題 此範例的決定
什麼會跨越邊界? 描述形狀種類的版本化值
誰可以接收此表示內容? 僅限來源 app
成功放置會做什麼? 在集合中附加一個具有新識別碼的項目
誰擁有變更權? 目的地檢視畫面的集合狀態
取消或拒絕時會怎樣? 模型不變
放置位置代表什麼? 此處不代表任何意義;接受的項目會依序附加

這些是應用程式設計決策。拖曳預覽呈現負載,而目的地決定負載如何改變模型。將這些職責分開,就能在不重寫集合規則的情況下更換傳輸方式。

選擇能表達契約的最精簡 API

情況 起始選擇 原因
SwiftUI 來源與目的地,macOS 13 或更新版本 Transferable、.draggable 和具型別的 .dropDestination 直接表達接受的模型型別
現有的 NSItemProvider 整合 .onDrag 和 .onDrop 保留明確的表示內容註冊與載入流程
部署目標早於 macOS 13 檢查該目標可用的 provider 修飾子 具型別的基礎方案需要 macOS 13;下文討論的 provider 多載需要 macOS 11
現有 AppKit 檢視畫面負責滑鼠互動 AppKit 拖曳 API 保留檢視畫面的事件處理和拖曳工作階段生命週期

Apple 說明了 macOS 13 起提供的 Transferable 和原始具型別放置目的地。以 provider 為基礎、接受 UTType 值陣列的 onDrop(of:isTargeted:perform:) 多載則從 macOS 11 起提供。請檢查個別多載,而不要假設所有拖放 API 的最低版本都相同。Transferable、dropDestination(for:action:isTargeted:)、onDrop(of:isTargeted:perform:)。

兩種做法都能限制在自有 app 內使用。對具型別的表示內容,套用 .visibility(.ownProcess);Apple 將此層級定義為僅在來源 app 內可見。因此,若只是要讓傳輸限於 app 內,沒有必要選用項目提供者。visibility(_:)、ownProcess。

建立具型別的複製互動

這個完整範例由新 macOS SwiftUI app 目標中的兩個 Swift 檔案組成。將部署目標設為 macOS 13 或更新版本,並在此基礎方案中使用 Swift 5 語言模式。用第二個檔案取代 Xcode 產生的 app 進入點;只保留一個 @main 宣告。此處已檢視程式碼,但因目前可用的 shell 沒有 Swift 工具鏈,因此未進行編譯。

宣告負載及其內容類型

在目標的資訊屬性清單中新增匯出型別宣告。在 Xcode 的屬性清單編輯器中,建立一個陣列 UTExportedTypeDeclarations,並在其中放入一個字典。將該字典的 UTTypeIdentifier 設為 com.example.shape-transfer,UTTypeDescription 設為 Synthetic Shape Transfer,並將 UTTypeConformsTo 設為包含 public.data 的陣列。此識別碼僅供示意;實際 app 請使用唯一識別碼。

Swift 宣告中的識別碼必須與屬性清單中的識別碼一致。Apple 要求自訂統一型別必須有對應的屬性清單項目,並說明 app 定義格式的匯出宣告。此範例傳輸的是資料而非檔案,因此不會加入檔名副檔名。為 app 定義檔案和資料類型。

建立 ShapePayload.swift:

import Foundation
import CoreTransferable
import UniformTypeIdentifiers

extension UTType {
    static let shapeTransfer = UTType(
        exportedAs: "com.example.shape-transfer",
        conformingTo: .data
    )
}

enum ShapeKind: String, Codable, Sendable {
    case circle = "Circle"
    case square = "Square"
}

struct ShapePayload: Codable, Sendable, Transferable {
    let version: Int
    let kind: ShapeKind

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .shapeTransfer)
            .visibility(.ownProcess)
    }
}

struct PlacedShape: Identifiable {
    let id = UUID()
    let kind: ShapeKind
}

CodableRepresentation 負責在負載與可傳輸資料之間轉換。負載包含的是一個值,而非檢視畫面或可變的集合參照。明確宣告遵循 Sendable,符合 Swift 的規則:可傳送結構中儲存的成員本身也必須可傳送。Transferable、Sendable。

這裡刻意只提供一種表示內容。文字代理會增加另一種使用項目的方式,但對此契約毫無用處。Apple 說明了表示內容的排序與備援格式;只有在接收端確實需要時才加入這些內容。將檢視畫面轉換為拖曳來源。

接受負載並提交一次集合更新

建立 ShapeTransferApp.swift:

import SwiftUI

@main
struct ShapeTransferApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeTransferView()
        }
    }
}

@MainActor
struct ShapeTransferView: View {
    @State private var placed: [PlacedShape] = []
    @State private var targeted = false
    @State private var message = "Drag a shape into Collection."

    var body: some View {
        HStack(alignment: .top, spacing: 24) {
            VStack(spacing: 16) {
                Text("Palette").font(.headline)
                paletteItem(.circle)
                paletteItem(.square)
            }
            .frame(width: 160)

            VStack(alignment: .leading, spacing: 12) {
                Text("Collection").font(.headline)
                Text(message)
                ForEach(placed) { item in
                    Label(item.kind.rawValue,
                          systemImage: symbol(for: item.kind))
                }
                Spacer(minLength: 20)
            }
            .padding()
            .frame(width: 300, height: 320, alignment: .topLeading)
            .background(targeted ? Color.blue.opacity(0.15)
                                 : Color.gray.opacity(0.08))
            .contentShape(Rectangle())
            .dropDestination(
                for: ShapePayload.self,
                action: { payloads, _ in
                    guard !payloads.isEmpty,
                          payloads.allSatisfy({ $0.version == 1 })
                    else {
                        message = "Drop rejected: unsupported payload."
                        return false
                    }
                    let additions = payloads.map {
                        PlacedShape(kind: $0.kind)
                    }
                    placed.append(contentsOf: additions)
                    message = "Added \(additions.count) shape(s)."
                    return true
                },
                isTargeted: { targeted = $0 }
            )
        }
        .padding(24)
    }

    private func paletteItem(_ kind: ShapeKind) -> some View {
        Label(kind.rawValue, systemImage: symbol(for: kind))
            .padding()
            .background(Color.gray.opacity(0.12))
            .draggable(ShapePayload(version: 1, kind: kind))
    }

    private func symbol(for kind: ShapeKind) -> String {
        switch kind {
        case .circle: return "circle.fill"
        case .square: return "square.fill"
        }
    }
}

明確使用 action: 和 isTargeted: 標籤,即可選取原始的具型別目的地多載。Apple 將其布林值結果定義為成功或失敗,並將位置參數定義為目的地檢視畫面的座標空間。此處刻意不使用位置,因為集合只會附加形狀,不會依位置排列。dropDestination(for:action:isTargeted:)。

接受規則會在附加任何項目之前,先檢查收到的每個負載。版本不受支援時,整批拒絕;每個有效負載都會取得新的目的地識別碼。因此,重複拖曳同一個調色盤項目會建立另一份副本。此規則適用於傳入閉包的值;解碼失敗會發生在它們成為 ShapePayload 值之前。

目標狀態只會改變呈現方式。它不會插入項目,也不會從來源移除任何內容。這種分工也讓取消時的結果很明確:若未接受放置,集合就維持不變。

在整合需求下使用項目提供者

provider 實作可以傳送相同的版本和形狀種類。保持契約不變,並依照以下步驟替換傳輸方式:

  1. 在 .onDrag 閉包中建立 NSItemProvider,並以相同的自訂型別識別碼註冊資料表示內容,使用 .ownProcess 可見性。
  2. 在載入處理常式中擷取不可變的負載值或預先備妥的位元組。編碼該值,並透過表示內容完成處理常式傳回資料或錯誤。
  3. 設定 .onDrop 以接受自訂型別。
  4. 在放置動作中,立即使用 loadDataRepresentation(forTypeIdentifier:completionHandler:) 開始載入每個接受的 provider。
  5. 解碼傳回的資料、驗證版本並收集結果。只有在取得所需結果後,才於主執行者上進行後續集合變更。

Apple 說明了 provider 的明確資料註冊方式和 app 內可見性。其載入方法為非同步,並會傳回進度物件。型別識別碼相符,只表示可以嘗試載入;不代表載入和解碼一定成功。registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:)、NSItemProviderRepresentationVisibility.ownProcess、loadDataRepresentation(forTypeIdentifier:completionHandler:)。

有兩個時間點需要留意。第一,Apple 指出 provider 載入必須在 .onDrop 動作內開始,但可以稍後才完成。若將初始載入呼叫移到稍後在另一個 actor 上執行的工作中,就違反了這項文件規定。第二,Apple 警告不要在表示內容處理常式中將匯出工作派送到主執行者或主執行緒,因為這可能造成掛起。匯出工作應獨立於 UI 變更;匯入完成後再更新集合狀態。onDrop(of:isTargeted:perform:)、onDrag(_:)。

對非同步 provider 實作,請區分接受載入工作和完成模型交易。以索引結果保留 provider 順序,回報載入或解碼失敗;若契約要求每個項目都成功,就讓集合維持不變。避免用篩選操作默默丟棄失敗項目。這些實作規則是從範例的整批接受政策推導而來。

保留 AppKit 的事件擁有權與移動語意

如果現有 AppKit 檢視畫面已負責滑鼠互動,就讓它繼續在該處啟動工作階段。Apple 的 beginDraggingSession(with:event:source:) 會接收起始滑鼠按下事件、使用該檢視畫面座標的拖曳項目框架,以及遵循 NSDraggingSource 的來源物件。來源回呼涵蓋工作階段的開始、移動,以及完成或取消。beginDraggingSession(with:event:source:)。

AppKit 的表示內容邊界也很明確:NSDraggingItem(pasteboardWriter:) 需要 NSPasteboardWriting 物件。SwiftUI provider 回呼和 AppKit 貼上板寫入物件是不同的介面。將自訂 AppKit 檢視畫面橋接到 SwiftUI 時,讓 AppKit 檢視畫面繼續負責自己的事件與工作階段;讓共用模型負責接受後的集合變更。init(pasteboardWriter:)。

此範例執行的是複製。若任務改為在集合間移動現有項目,請加入穩定的項目識別碼,並明確指定唯一的移除負責者。共用儲存區可以移動項目一次;或者目的地可以先插入項目,再由來源於確認移動完成後移除。混用兩種策略可能會導致項目被移除兩次。

目前的 Apple 文件包含 macOS 26 提供的 DragConfiguration、以工作階段為基礎的具型別放置多載,以及拖曳工作階段觀察功能。來源會提出支援的操作,目的地選擇操作,而工作階段完成後,來源即可回應結果。採用此路徑前,請檢查每個符號的 SDK 可用性。DragConfiguration、dropDestination(for:isEnabled:action:)、onDragSessionUpdated(_:)。

當指標進入目的地或預覽出現時,不要移除來源項目。這些事件都早於接受放置。若要在 app 內移動項目,應遵守的關鍵條件是交易完成後,一個邏輯項目只屬於一個集合。

以可觀察結果驗證契約

在 Mac 上依照以下順序檢查實作:

  • 將 Circle 拖到 Collection:目的地新增一個圓形,調色盤則保留原有項目。
  • 重複拖曳:目的地新增第二個具有獨立識別碼的項目。
  • 將 Square 拖到目的地以外並放開:集合不變。
  • 暫時將來源負載版本改為 2:目的地回報拒絕且不新增任何內容;之後還原為 1。
  • 若使用 provider 實作,刻意傳回編碼或載入錯誤:顯示失敗,但不插入不完整批次。
  • 若使用移動實作,取消拖曳並確認原項目仍屬於原本的集合。

對這個雙窗格複製任務,請選擇具型別的 SwiftUI,除非有明確的相容性或整合需求要求使用 provider 或 AppKit。真正持久的部分是契約:定義好的值會跨越邊界,目的地會驗證該值,而一位模型擁有者會提交已接受的變更。

來源