从零读懂 SwiftUI 新拖放 API:用 Apple 的纸牌示例理解排序、多项拖拽与移动规则

拖动一张卡片,让旁边的卡片自动让位;一次搬走一叠卡片;松手时,只有符合规则的位置才接收。

交互示意动画,非官方示例实机录屏。

这些看似相近的交互,实际上包含三个不同的问题:内容怎样重新排序,一次拖走哪些数据,目标是否允许这次操作。

Apple 在 WWDC26 的《Code-along: Build powerful drag and drop in SwiftUI》中,用一款 Solitaire 纸牌游戏把这三个问题串了起来。

本文参考官方视频、代码、下载工程和 API 文档,按"先理解概念,再读排序,最后读数据转移"的顺序拆解。读完后,你应该能回答:

  • reorderable 和 reorderContainer 为什么要一起使用?
  • ReorderDifference 给的是新数组,还是移动指令?
  • dragContainer 怎样决定一次带走哪些项目?
  • DragConfiguration(allowMove: true) 为什么不会自动帮你搬数据库记录?
  • dropConfiguration 与 dropDestination 分别应该做什么?
  • 为什么相同的 CardValue 类型既可能表示数据,也可能表示 ID?

阅读与核对说明:核对日期为 2026-10-20。讲座为 WWDC26 Session 271,介绍面向 2027 系统版本的新能力。下载工程的 App Target 为 macOS 27.0,使用 SwiftUI 与 SwiftData。

一、先找到官方工程

官方入口:

长期收藏建议使用示例文档入口,下载地址可能更新。

工程主线很清楚:

text 复制代码
SwiftUI-Reorderable/
├── Game/
│   ├── CardValue.swift       牌面值与 Transferable 表示
│   ├── Card.swift            SwiftData 中的一张牌及其位置
│   └── Game.swift            移动、插入、验证规则
└── Views/
    ├── GameView.swift        整个牌桌、排序容器与落点配置
    ├── PileView.swift        某一列牌
    ├── RemainderView.swift   剩余牌堆和当前可拖出的牌
    ├── DestinationView.swift 按花色收集牌的目标区
    └── CardView.swift        单张牌的显示

第一遍不必读完布局实现。先看 CardValue、GameView 和 Game,就能把"拖了什么、放到哪里、数据怎样变"连起来。

二、先分清哪些是新 API,以及"新"是哪个版本

不能把这场讲座里的所有类型都统一写成"iOS 27 才出现"。

本次逐项核对的官方符号文档标注如下。表格聚焦 iOS、iPadOS、macOS 和 visionOS,不是所有平台的完整支持清单:

符号 iOS / iPadOS macOS visionOS
reorderable() 27.0 27.0 27.0
本文使用的多集合 reorderContainer 重载 27.0 27.0 27.0
ReorderDifference 27.0 27.0 27.0
dragContainer(for:in:_:) 27.0 26.0 27.0
dragConfiguration(_:) 27.0 26.0 27.0
dropConfiguration(_:) 27.0 26.0 27.0
DragConfiguration / DropConfiguration 类型 26.0 26.0 26.0
DropSession 类型 26.0 26.0 26.0
接收 DropSession 的 dropDestination 重载 26.0 26.0 26.0
dragPreviewsFormation(_:) 本次文档未列出 26.0 本次文档未列出
dropPreviewsFormation(_:) 本次文档未列出 26.0 本次文档未列出

这里有两个容易忽略的区别:

  1. 配置类型与使用它的修饰符是不同符号,可能有不同的可用版本。
  2. 视频介绍的平台范围与当前符号页面不完全一致。视频说多项拖拽相关修饰符扩展到 iOS、iPadOS、visionOS 27,但本次取得的两个预览队形页面只标 macOS 26。

因此,不能仅凭视频中的一段 macOS 示例,断言同一段预览配置已获得所有移动平台文档确认。跨平台接入时应再核对目标 SDK 的声明与 availability;"文档未列出"也不等于本文证实了永久不支持。

基础的 .draggable(...) 和较早的 .dropDestination(for:action:isTargeted:) 在 iOS 16 时期就已存在。本文后面使用的"回调接收 DropSession"写法,应与老的"接收位置 CGPoint、返回 Bool"重载分开阅读。

三、Transferable:先让系统知道怎样传递一张牌

对应 Game/CardValue.swift。官方节选:

swift 复制代码
nonisolated struct CardValue: Hashable, Sendable, Codable {
    var rank: Rank
    var suit: Suit
}

extension CardValue: Identifiable {
    var id: Self { self }
}

extension CardValue: Transferable {
    nonisolated static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .cardValue)
    }
}

Rank 是点数,Suit 是花色,定义见原文件。

逐个翻译:

  • Codable:这个值可以编码和解码。
  • CodableRepresentation:告诉系统使用可编码表示来传输。
  • .cardValue:示例自定义的 UTType,标识"这是一种牌面数据"。
  • Identifiable:系统能稳定地认出是哪张牌。
  • Sendable:类型满足相应的并发传递要求。

.cardValue 是类型标识,不是图片名,也不是数据库表名。对方 App 只有支持兼容的表示,才能理解这些内容;声明 Transferable 不等于所有 App 都会接收纸牌。

为什么 CardValue 本身可以当 ID?

因为这个示例是一副标准牌,同一牌局里"点数 + 花色"可以识别某张牌,所以 id 直接返回自身。

这就解释了后面的类型:

swift 复制代码
ReorderDifference<CardValue, Card.Group>

第一个泛型参数表达的是 ItemID 。只是恰好 CardValue.ID == CardValue,看上去才像直接传了一张牌。

如果做双副牌游戏,或者允许同名同图的卡片重复出现,就需要独立的稳定标识,例如 UUID,不能继续认为相同牌面就是同一个项目。

四、reorderable:声明"哪些内容可以重新排序"

视频 3:40 的 Preview 先从四张牌开始。官方节选:

swift 复制代码
HStack {
    ForEach(cards) { card in
        CardFaceView(card: card)
    }
    .reorderable()
}
.reorderContainer(for: CardValue.self) { difference in
    cards.apply(difference: difference)
}

这里两行修饰符分工不同:

  • .reorderable() 放在 ForEach 上,标记这批动态内容参与排序。
  • .reorderContainer(...) 放在外层布局上,规定排序作用域并接收结果。

reorderable() 定义在 DynamicViewContent 上。不要把它理解为任意单张 CardFaceView 都能随便添加的通用 View 修饰符。

拖动过程中,SwiftUI 负责抬起、占位和其他视图让位。真正松手后,系统把差异交给闭包,App 再更新数据源。

视觉上已经让位,不代表数组已经保存为新顺序。

五、reorderContainer:参数分别是什么意思?

多集合版本的声明可以这样读,以下为简化展示:

swift 复制代码
reorderContainer(
    for: Item.Type,
    in: CollectionID.Type,
    isEnabled: Bool = true,
    move: (ReorderDifference<Item.ID, CollectionID>) -> Void
)
参数 含义 示例
for 参与排序的数据项类型 CardValue.self
in 用来识别各集合的类型 Card.Group.self
isEnabled 当前是否允许排序,默认 true 保存过程中可以关闭
move 操作结束时接收差异的回调 调用业务方法更新位置

CardValue.self 表示类型本身,不是某张牌,也不是闭包中的 self 对象引用。

单集合时可以省略 in,使用单集合便利重载。数据没有采用 Identifiable 时,还有通过 itemID KeyPath 显式指定标识的重载。

系统基于稳定 ID 描述移动,而不只是返回拖动开始时的数组下标。这有助于在拖动期间数据发生变化时重新定位内容,但仍需要 App 处理记录被删除、目标不存在或业务状态变化。

六、ReorderDifference:拿到的究竟是什么?

可以把它理解为一张"搬运单":

text 复制代码
sources:要移动的项目 ID,带有顺序
    ↓
destination.collectionID:放到哪个集合
    ↓
destination.position:放在某个 ID 前面,或者放到末尾

在示例中,目标位置主要涉及:

  • .before(某个项目ID):插到该项目之前。
  • .end:追加到集合末尾。

这不是标准库的 CollectionDifference,也不是一整个新数组。

cards.apply(difference:) 不是系统自动提供的数组能力

这个方法定义在官方 GameView.swift 底部的 extension Array 中。

它先找来源、移除来源,再根据目标插入。示例实现只读取 difference.sources[0],服务于单张牌 Preview,不能直接当成通用的批量移动算法。

下面是一个单项排序的讲解改写,先在副本上处理,全部确认后再替换原数组:

swift 复制代码
func moveOne<CollectionID: Hashable & Sendable>(
    _ difference: ReorderDifference<CardValue.ID, CollectionID>,
    cards: inout [CardValue]
) {
    guard difference.sources.count == 1,
          let sourceID = difference.sources.first,
          let sourceIndex = cards.firstIndex(of: sourceID) else {
        return
    }

    var updatedCards = cards
    let movedCard = updatedCards.remove(at: sourceIndex)

    switch difference.destination.position {
    case .before(let targetID):
        guard let targetIndex = updatedCards.firstIndex(of: targetID) else {
            return
        }
        updatedCards.insert(movedCard, at: targetIndex)
    case .end:
        updatedCards.append(movedCard)
    }

    cards = updatedCards
}

这里的 CollectionID 使用泛型,因此也能接收单集合便利重载产生的集合标识类型;并不要求调用方一定使用 Card.Group。该方法明确只支持一个来源。多个来源需要按移动顺序整体取出、统一移除、定位目标,再整体插入;跨集合还需要同时更新来源与目标。

七、多列排序:collectionID 不是卡片 ID

纸牌有七列,必须区分"哪张牌"和"哪一列"。

官方在牌桌的公共父布局上声明:

swift 复制代码
.reorderContainer(
    for: CardValue.self,
    in: Card.Group.self
) { difference in
    game.moveCards(difference: difference)
}

每一列内部对应的写法,可以用下面的讲解改写表达:

swift 复制代码
// faceUpIndex 是正面牌起点,pileIndex 是列号,二者含义不同。
ForEach(cards[faceUpIndex...], id: \.value) { card in
    CardView(card: card)
}
.reorderable(collectionID: Card.Group.pile(pileIndex))

这里要求 faceUpIndex 是合法的切片边界,pileIndex 是实际列号。下载源码在 PileView 中用局部 let index = firstFaceUpIndex 遮蔽了同名列号属性,随后的 .pile(index) 也引用该局部值;这是静态阅读发现的潜在集合标识问题。因此本文用两个不同名字解释应有语义,不把原片段作为已验证实现。

Card.Group.pile(0)、.pile(1) 等标识不同集合。共同父容器把这些集合纳入同一排序范围,于是可以从第一列搬到第二列。

text 复制代码
reorderContainer:牌桌上的可排序区域
    ├── collectionID = .pile(0)
    ├── collectionID = .pile(1)
    └── collectionID = .pile(2) ...

不要给每一列各建一个彼此独立的排序容器,再期待系统自然把它们当作同一个跨列排序区域。

不允许拖动背面牌,最简单的方法是什么?

官方把牌分成两个 ForEach:背面牌不加 .reorderable,正面牌才添加。

这个设计很实用:如果内容本来就不应该参与交互,可以先从结构上排除,而不是把所有内容都变成可拖动后,再写一堆拒绝逻辑。

八、dragContainer:一次拖哪些项目,由业务决定

排序默认可以先从单项做起。但纸牌中,拖动某张牌时,需要连同压在它上面的牌一起带走。

视频与下载工程使用:

swift 复制代码
// 官方工程节选:此处按单个起拖 ID 使用。
.dragContainer(for: CardValue.self) { cardID in
    game.cardStack(startingAt: cardID)
}

cardStack(startingAt:) 是业务方法。它找到这张牌,筛选同一组中位置不小于它的牌,再按位置排序并返回牌面值。

不是拖动整列容器,而是一次拖动的 payload 包含多个项目。

单 ID 与 ID 数组:这是两个重载,不是版本冲突

官方文档提供了两个同名重载,区别在 payload 回调的输入:

swift 复制代码
// 单个起拖项目的 ID。
(Item.ID) -> Data

// 一组项目的 ID。
(Array<Item.ID>) -> Data

二者都返回元素为 Item 的集合,所以即使输入只有一个 ID,也可以返回多张牌,实现整叠拖动。视频中的 game.cardStack(startingAt: cardID) 正是这种用法。

数组版本适合结合选择状态组装 payload:起拖项目属于选择集合时,回调收到选择中的 ID;否则只包含起拖项目的 ID。返回空集合可以阻止本次拖动。

如果只是复现纸牌的"从某张牌取整叠",可以显式标注参数类型,明确使用单 ID 重载:

swift 复制代码
.dragContainer(for: CardValue.self) { (cardID: CardValue.ID) in
    game.cardStack(startingAt: cardID)
}

如果采用数组重载并接入多选,应处理整组 ID,明确合并、排序与去重规则,不能只取第一个 ID 就认为实现了多选拖动。

普通 App 的多项选择如何接入?

当前官方文档给出的模式是:

swift 复制代码
// 接口组合示意,需补齐项目类型、选择状态和数据查询。
.draggable(containerItemID: item.id)

子视图标明自己对应的 ID;共同父视图用 dragContainer 生成传输值,再用 dragContainerSelection(...) 提供选择状态。

containerItemID 是查找入口,payload 是真正要传输的值,二者不要混为一谈。in 命名空间参数则可以用来识别拖拽容器;简单场景使用默认值即可。

九、两种 formation:管的是预览外观

官方工程使用:

swift 复制代码
.dragPreviewsFormation(.stack)

以及外层的:

swift 复制代码
.dropPreviewsFormation(.stack)

视频列出 pile、list、stack 等队形。

  • dragPreviewsFormation 控制起拖预览的排列。
  • dropPreviewsFormation 控制经过接收区域时的预览排列。

只设置前者,卡片进入接收区域后,可能又采用目标的默认队形,所以示例还在牌桌外层统一设置后者。

队形不会改变数据库顺序,也不会决定拖走哪些牌。 它只是让一次多项拖动看起来更合适。

平台方面仍需遵守第二节的核对结果:当前两个符号文档只列 macOS 26;本机 Xcode 26.4.1 的 SwiftUI 接口也明确将它们标记为 iOS、visionOS 不可用。这验证了 26 系列 SDK 的限制,不代表已经核对 27 系列 SDK,本文不将它们作为已确认的全平台代码。

十、dragConfiguration:源头表达"我允许移动"

剩余牌堆与七列排序区域是两个不同入口。玩家从剩余牌堆拖出一张牌,不能在目标处复制出第二张相同牌。

官方添加:

swift 复制代码
.dragConfiguration(DragConfiguration(allowMove: true))

这里 allowMove 是"允许 move",不是"强制 move",也不是"禁用 copy"。官方文档说明默认允许复制,最终操作由接收目标决定。

可以把它理解为协商:

text 复制代码
拖出方:我支持哪些操作
    ↓
会话:当前建议的操作集合
    ↓
目标方:我选择接收为 move、copy,或者禁止
    ↓
业务层:真正修改数据

如果源支持移动,目标却选择复制,那么仍然是复制。纸牌的目标只接受合法的 .move,才形成不会复制牌的约定。

十一、dropDestination:接收数据并落实变更

reorderContainer 默认处理容器内部的移动。要接收从剩余牌堆等容器外部拖入的项目,需要继续组合 dropDestination。

官方节选:

swift 复制代码
.dropDestination(for: CardValue.self) { newCards, session in
    if let destination = session.reorderDestination(
        for: CardValue.self,
        in: Card.Group.self
    ) {
        game.insertCards(newCards, to: destination)
    }
}

参数含义:

参数 / 方法 含义
for: CardValue.self 要接收哪一种传输值
newCards 本次收到的值数组
session 本次投放会话,类型为 DropSession
reorderDestination(for:in:) 获取关联排序容器中的目标集合与目标位置,可能为空

官方把这个修饰符声明在 reorderContainer 之后:内部重排交给排序闭包,外部拖入交给投放闭包。

"外部"首先指排序容器外部,并不自动意味着来自其他 App。这个纸牌工程在后面的规则验证中依赖 localSession,并没有提供通用的跨 App 导入流程。

注意新旧 dropDestination 的区别

本节代码的第二个参数是 DropSession。这一会话型 dropDestination 重载从 iOS、macOS、visionOS 26 起提供;但其中用于排序容器的 reorderDestination 属于 27 系列新增能力,不能因为前者支持 26,就认为整段排序代码也支持 26。

旧教程中常见的第二个参数是 CGPoint,通常命名为 location,action 返回 Bool。不要把旧重载里的 location 改名为 session,就以为获得了新会话能力;最终调用哪个重载取决于 SDK 提供的真实方法声明。

十二、dropConfiguration:把落点与游戏规则连接起来

dropDestination 做"收到之后怎么处理",dropConfiguration 做"当前这样放,允许吗"。

官方核心节选:

swift 复制代码
.dropConfiguration { session in
    let alignedX = session.location.x - 0.5 * spacing
    let pile = Int(alignedX / (cardWidth + spacing))
    let destination = ReorderDifference<CardValue, Card.Group>
        .Destination(position: .end, collectionID: .pile(pile))

    let allowed = session.suggestedOperations.contains(.move)
        && game.validateMove(session: session, destination: destination)
    let operation: DropOperation = allowed ? .move : .forbidden

    return DropConfiguration(
        operation: operation,
        destination: destination
    )
}

这段代码完成三件事:

  1. 根据会话位置推算玩家指向哪一列。
  2. 把目标明确设为该列末尾。
  3. 检查来源支持移动,并让游戏规则决定允许还是禁止。

几个新名字逐个解释

符号 含义
session.location 会话在该接收视图中的位置,不应当作屏幕全局坐标
suggestedOperations 本次建议的操作集合,用来检查是否有 .move 等操作
DropOperation 目标选择的投放操作
.forbidden 当前不允许投放,目标不应接收这些牌
DropConfiguration 返回给系统的接收策略
Destination(position:collectionID:) 告诉排序系统插入到哪里

Card.Group.pile(pile) 是某一列的标识;.end 表示列尾,符合这个示例把牌叠到列尾的规则。

配置闭包可能随着指针位置变化被频繁调用。官方文档明确提醒不要做昂贵计算。这里适合读当前状态、命中测试、计算策略;网络请求、数据库写入等副作用应留在真正提交的业务路径。

坐标计算需要边界保护

原示例的横坐标换算较简洁,迁移时至少应核对:

  • 当前宽度是否足够使 cardWidth 与步长有效。
  • 指针是否还在有效牌列区域内。
  • 算出的列号是否位于 0..<7。
  • 列间空隙是拒绝,还是归属相邻列。
  • 拖动途中布局尺寸变化后,命中区域是否仍正确。

不要只依赖 Int 截断就认为坐标已经可靠落在某一列。更复杂的布局可以使用实际子视图边界做命中测试。

十三、localSession:为什么这个示例主要验证 App 内拖动?

Game.validateMove(session:destination:) 会读取:

swift 复制代码
session.localSession?.draggedItemIDs(for: CardValue.self)

当前官方 DropSession 文档说明,localSession 用于识别从自己 App 内部开始的拖动,外部来源时为空。

示例取第一张拖动牌,判断它是否能接到目标列上。不同花色颜色交替、点数递减,才允许接到非空列末尾。

这也说明:

  • Transferable 定义了传输能力。
  • 目标是否接受该来源,由本 App 规则决定。
  • 使用 localSession 的这条验证路径,并没有证明支持陌生 App 拖来的牌。

如果业务确实允许外部导入,需要另行设计数据验证、导入去重与落点规则,不能把本地 ID 当作外部数据的可信证明。

十四、move 如何真正落地?答案在 Game 里

官方 Game.moveCards(difference:) 的核心是:

swift 复制代码
func moveCards(difference: ReorderDifference<CardValue, Card.Group>) {
    let cards = difference.sources.compactMap { value in
        cards.first { $0.value == value }
    }
    appendCards(cards, to: difference.destination.collectionID)
}

其中 appendCards 更新现有 Card 的:

  • groupID:它现在属于哪个组。
  • position:它在组里的顺序。

然后修正来源组的序号,并在适用时翻开来源列最后一张背面牌。

insertCards 也会用收到的 CardValue 在当前牌局中找出已有 Card,再调用同一套搬运逻辑。它没有简单地创建一批新的 SwiftData 记录。

这个示例不会复制牌,是因为业务层重新归属已有对象。 不能把效果全部归功于 allowMove: true。

如果来源和目标不是同一份模型呢?

官方 DragConfiguration 文档给出的模式是:观察 .onDragSessionUpdated,确认会话以 .ended(.move) 结束后,再执行来源清理。

但在当前纸牌工程中,目标已经移动了共享模型里的原对象,来源视图通过数据变化更新。如果再照搬一段"成功后删除源 Card"的逻辑,反而可能把刚搬过去的牌删掉。

因此要先确定模型所有权:是同一对象改归属,还是目标导入、来源清理。删除时机、失败恢复与事务一致性都属于业务层设计。

十五、拖动取消时,隐藏的牌怎样恢复?

RemainderView 还有一段容易被忽略的生命周期处理:

swift 复制代码
.onDragSessionUpdated { session in
    switch session.phase {
    case .initial, .active:
        hiddenCard = session.draggedItemIDs(for: CardValue.self).first
    case let .ended(operation):
        if operation == .forbidden || operation == .cancel {
            hiddenCard = nil
        }
    default:
        break
    }
}

以上为官方节选。hiddenCard 是这个 SwiftUI 结构体视图的状态。

起拖时先隐藏来源处那张牌,避免预览与原视图同时出现。禁止或取消时恢复;成功移动后,再结合当前牌变化更新显示。

onDragSessionUpdated 是观察拖动生命周期的入口,不等于最终数据库事务一定成功的证明。把 UI 清理与业务数据变更区分开,才能处理好取消、失败和下一张牌显示的时机。

十六、官方示例的简化,迁移时必须看清

这部分来自源码静态阅读,不是运行故障结论。

1. 牌局规则经过简化

validateMove 对空列直接返回 true,允许任意牌进入;不能把它描述成完整实现了"空列只能放 K"的常见 Klondike 规则。

2. 游戏层总是追加到末尾

moveCards 与 insertCards 使用目标集合 ID,最终调用 appendCards,没有实现通用的 .before(...) 插入。

因为示例配置了列尾目标,这样能配合演示。做任务看板或照片排序时,需要真正处理目标位置,不能只改所属列。

3. 多项有效性依赖现有牌堆状态

规则验证主要检查拖动序列的第一张牌,而 cardStack 根据现有位置取得整叠。它依赖当前牌局维持了合法序列,不能推广为任意外部多项输入都已完整验证。

4. Preview 的数组扩展只支持单项

difference.sources[0] 没有先检查空数组,且只处理第一个来源。生产实现要处理空来源、批量顺序、目标消失及失败时保持原数据。

5. 同名重载与平台范围要分别核对

dragContainer 同时提供单 ID 和 ID 数组回调,阅读时要打开具体重载,不能只看同名汇总页。预览队形的当前文档平台范围与视频描述不完全一致,也应结合目标 SDK 核对。

这些边界不影响理解设计思想,但会影响能否直接迁移到真实项目。

十七、在自己的项目里,建议按这个顺序接入

  1. 给项目设置稳定 ID,先确认重复内容不会共享身份。
  2. 只对一个集合接入 reorderable 与 reorderContainer。
  3. 在业务层正确处理 .before 与 .end,先验证数据更新。
  4. 扩展为多个集合,为各集合提供稳定 collectionID。
  5. 需要批量拖动时,再决定是多选,还是像纸牌一样由业务推导一组项目。
  6. 用 dragContainer 返回真正传输的数据。
  7. 接收容器外内容时添加 dropDestination。
  8. 使用 dragConfiguration 与 dropConfiguration 协调复制、移动及规则。
  9. 补上取消、拒绝、数据变化和来源清理。
  10. 最后调整预览队形等视觉效果,并按目标平台核对可用性。

如果采用 MVVM,让 View 负责声明修饰符、转发会话与呈现状态;由 ViewModel 或业务服务统一实施排序、规则验证和持久化。不要把移动算法、网络同步和错误恢复都塞进 View 的配置闭包。

推荐验收场景:同列向前与向后移动、跨列移动、多项保持顺序、空列、非法落点、拖到区域外取消、拖动期间目标被删除、保存失败以及重复触发提交。

本文核对了官方源码、在线符号文档及本机 Xcode 26.4.1 的相关 SwiftUI 接口声明;没有使用 27 系列 SDK 编译完整示例,也未执行模拟器运行。讲解改写用于说明实现思路,不作为已编译验证的完整工程代码。

十八、把整条主线串起来

纸牌示例的完整思路可以表示为:

text 复制代码
Transferable:数据怎样传
    ↓
reorderable + reorderContainer:哪些内容在哪个范围排序
    ↓
dragContainer:一次实际带走哪些项目
    ↓
dragConfiguration:来源允许哪些操作
    ↓
dropConfiguration:当前位置接不接受、采用什么操作、落在哪里
    ↓
排序闭包 / dropDestination:把动作交给业务层
    ↓
Game:更新归属与顺序,驱动视图刷新

掌握这条主线后,纸牌可以换成照片、待办事项、书签或者看板卡片。SwiftUI 负责提供拖放交互与结构化上下文,App 负责定义数据与操作真正代表什么。

参考资料与代码来源

以下视频、示例及符号文档均在本次写作中实际读取核对:

Transferable 的前置视频链接用于扩展阅读;本文相关代码核对来自下载工程的 CardValue.swift。官方原始版权与许可见工程中的 LICENSE.txt。

相关推荐
时间的情敌2 小时前
Electron 设置-webkit-app-region: drag;实现拖拽后, 无法响应鼠标划过事件及点击事件的解决方式
前端·vue.js
程序猿阿森2 小时前
深入解析 NLI:RAG 与大模型防幻觉的“最后一道防线”
前端·人工智能·算法
iCxhust4 小时前
模型:500kW飞轮 10kV输电线路 油田抽油机 同时考虑无功补偿 统计:全年节电量 结果56万度电
前端·css·css3·电力·飞轮
志尊宝4 小时前
AI原生前端开发(011):Chrome 148+内置Gemini Nano入门
前端·chrome·ai-native
Liu.7744 小时前
uniapp swiper 常见问题及修复
前端·uni-app
u0111026754 小时前
图片生成任务的日志记录与敏感信息取舍
前端·图像处理·算法·ai作画
梦想很大很大5 小时前
重新思考 Workrun:AI 时代,我们还需要手动编排 Workflow 吗?
前端·人工智能·后端
小羊没烦恼!5 小时前
深入浅出SQL Server中的死锁
java·开发语言·前端·数据库·后端·spring·c#