
序幕:从龟仙人的训练场到 SwiftUI 的重排宇宙
布尔玛: "喂!悟空,贝吉塔,还有那边那个光头克林!天下第一武道会的登记表被你们弄乱了!谁准你们把弗利萨排在第一位的?!"
孙悟空(挠头笑): "嘿嘿,因为弗利萨看起来很强嘛,我想先跟他交手!"
贝吉塔(双手抱胸,冷哼): "哼,卡卡罗特。在战斗中,出场顺序就是一切。我怎么可能排在雅木茶后面?!"
在过去,如果我们想在 SwiftUI 的 List 里调整这群宇宙级战士的出场顺序,通常只有两条路:要么让用户点击"编辑"按钮,小心翼翼地拖动系统给的灰色小把手;要么就得像比克(Piccolo)训练悟饭那样,一拳一脚地去手搓 DragGesture 物理碰撞逻辑。

好在,WWDC26(iOS 27)为 SwiftUI 带来了更直接的"合体必杀技":.reorderable() 与 .reorderContainer(...)。

今天,我们就借着这个龙珠测试项目,把这套全新招式从入门到实战彻底讲透。
第一章:天下第一武道会的出场顺序,为什么不能只看 UI?
在 ContentView.swift 里,武道会的前台接待处非常清爽:一个 NavigationStack,两个分会场入口。
swift
NavigationLink {
ListReorderDemoView()
} label: {
DemoLinkRow(
title: "List 重排演示",
subtitle: "使用 List 测试 reorderable()",
imageName: "B1"
)
}
NavigationLink {
LazyVGridReorderDemoView()
} label: {
DemoLinkRow(
title: "LazyVGrid 重排演示",
subtitle: "使用 LazyVGrid 测试 reorderable()",
imageName: "B2"
)
}
这两个入口分别对应了本次 SwiftUI 重排新特性的两个典型战场:
List(传统擂台):一行一个选手,纪律严明。LazyVGrid(龙珠雷达网格):一屏多个格子,错落有致。

布尔玛的技术笔记:
"大家注意,重排的重点从来都不是'卡片能不能在屏幕上飘起来',而是'松手的那一瞬间,底层的数据模型有没有老老实实地跟着变'。SwiftUI 这次的设计非常聪明,它把重排拆成了两步:"
- 谁能被拖着走? ------ 挂上属性
.reorderable()。 - 落地后数据怎么变? ------ 交给外层容器的
.reorderContainer(...)闭包。

UI 负责展示肉眼可见的"残像拳",而数据层则负责接住最终的"战斗结果"。
第二章:悟空先上场:List 里的最小可用重排
让我们先来看大弟子悟空在 ListReorderDemoView.swift 里的基本演示。
swift
struct ListReorderDemoView: View {
@State private var numbers = Array(0...9)
var body: some View {
List {
ForEach(numbers) { i in
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 18) {
Image("B\(i + 1)")
.resizable()
.scaledToFill()
.frame(width: 112, height: 112)
.clipShape(RoundedRectangle(cornerRadius: 10))
Text("\(i)")
.font(.system(size: 88))
}
}
}
.reorderable() // 标记:这里的每一行都是可拖动的战士
}
.reorderContainer(for: Int.self) { diff in // 告诉容器:这里是 Int 选手的专属擂台
diff.apply(to: &numbers) // 裁判宣布最终结果,直接修改数据源
}
}
}
龟仙人(拄着拐杖,戴着墨镜):
"咳咳,悟空啊,你这个代码能跑通,全亏了后山那段悄悄给
Int补上的'外挂':"
swift
extension Int: @retroactive Identifiable {
public var id: Int { self }
}

因为 .reorderContainer(for:) 内部有着严格的家规:被重排的类型必须符合 Identifiable 协议,以便系统能够跨线程安全地识别每个元素。

虽然我们在测试时可以用 retroactive 让标准库的 Int 直接用自身当 id,但在真正的生产项目(比如卡普空或万代的游戏开发)中,贝吉塔是绝对不允许这种偷懒行为的。更标准的"赛亚人写法"应该是定义明确的模型:
swift
struct Fighter: Identifiable, Sendable {
let id: Int // 唯一的战斗员编号
var name: String // 姓名,比如"孙悟空"
var imageName: String // 照片
}
这样既能避免给系统标准库类型乱打补丁,也能防止与其他模块发生协议冲突,稳如泰山。
第三章:贝吉塔不服:为什么是 ReorderDifference,而不是 From/To Index?
在过去,旧的列表重排我们通常使用 .onMove 1。系统会扔给你一个 IndexSet(源下标集)和一个目标 offset(偏移量),然后你调用数组的 move(fromOffsets:toOffset:)。
贝吉塔(气得头发更竖了):
"荒谬!愚蠢!卡卡罗特,如果弗利萨那家伙突然用气弹把克林炸飞了,整个数组的长度瞬间缩水,我原本在第 4 位的索引不就变成第 3 位了?!在这种瞬息万变的战场上,用不稳定的'视觉索引(Index)'来定位战士,简直就是找死!"
贝吉塔一针见血。WWDC26 彻底抛弃了脆弱的下标思维,转而引入了 ReorderDifference。
ReorderDifference 不再关心"第几行移动到第几行",而是记录 "谁(Identity)移动到了哪里":
sources:哪些战士被拖走了(保存的是这群战士的id,哪怕弗利萨中途乱入,这些id依旧唯一且正确)。destination:插入的目标位置(例如"插入到贝吉塔这个 ID 的前面",或者是"放到队伍末尾")。CollectionID:如果现场有多个擂台(比如多组 Section),指出这次移动发生在哪一个擂台上。

这样一来,即使你的数据源在拖拽过程中因为网络同步、后台刷新而发生改变,重排逻辑也不会发生"张冠李戴"的悲剧。
第四章:比克老师拆招:自定义的 apply(to:) 为什么要用 OrderedDictionary?
在这个演示项目中,我们并没有直接使用系统默认的 apply,而是自己手写了一个优雅的扩展。
比克(双臂抱胸,头蓬迎风飘扬):
"悟饭!别光看着!仔细看这段逻辑!这才是真正的气功运行路线:"
swift
import Collections // 引入官方 swift-collections 库
extension ReorderDifference
where CollectionID == ReorderableSingleCollectionIdentifier {
func apply(
to values: inout [some Identifiable<ItemID>]
) {
// 1. 先用当前的战士数组,生成一个以 ID 为 Key、战士本体为 Value 的有序字典
var dictionary = OrderedDictionary(
uniqueKeys: values.map(\.id),
values: values
)
// 2. 找到目标落脚点的 Offset 到底在当前字典的哪个位置
let destinationOffset: Int? =
switch destination.position {
case .before(let destination):
dictionary.keys.firstIndex(of: destination)
case .end:
nil
}
// 3. 将被拖拽的那批战士(sources),整体迁徙到目标落脚点
dictionary.move(
keys: sources,
to: destinationOffset ?? values.endIndex
)
// 4. 将调整完毕的字典重新写回原数组
values = dictionary.values.elements
}
}
比克老师解释道,这里引入 OrderedDictionary 主要有两个原因:
- 像普通字典一样通过 ID 查找 :能极速响应
ReorderDifference.sources这种基于"身份(Identity)"的移动指令。 - 像普通数组一样保持顺序:方便完美将排序结果呈现在 UI 上。

这正是"有序 + Key 检索"的完美结合,也是我们依赖 swift-collections 的底气所在。
第五章:龙珠雷达展开:LazyVGrid 也能用同一套重排模型
接着,我们把战场转移到 LazyVGridReorderDemoView.swift。
swift
struct LazyVGridReorderDemoView: View {
@State private var photos = Array(0...9)
private let columns = [
GridItem(.adaptive(minimum: 150), spacing: 16)
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(photos) { photoIndex in
VStack(alignment: .leading, spacing: 8) {
Image("B\(photoIndex + 1)")
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.aspectRatio(1, contentMode: .fill)
.clipShape(RoundedRectangle(cornerRadius: 10))
Text("图片 \(photoIndex + 1)")
}
}
.reorderable() // 瞧,也是只要在这里挂载即可!
}
.reorderContainer(for: Int.self) { diff in
diff.apply(to: &photos) // 同样的一招制敌!
}
}
}
}
布尔玛(得意地晃了晃手中的雷达):
"看吧!在我的雷达网格里,重排的代码几乎一字未改!这就是 WWDC26 重排 API 的最伟大之处------它不再是
List的专属特权!"
过去我们要写网格重排,手势检测、计算坐标、处理插值,代码写得像那美克星的历史一样冗长。

现在,不管是单列列表,还是动态网格,底层都共享着同一套 ReorderDifference 模型。

第六章:界王星的修炼禁忌:.reorderable() 应该挂在哪里?
北界王(抱着猩猩巴布鲁斯,一脸严肃):
"听好了悟空,如果你把气功的姿势摆错了,不仅打不中敌人,可能还会把界王星给炸了!这两个 Modifier 的位置绝对不能放反!"
我们必须牢记:
.reorderable()必须挂在ForEach的后面。它修饰的是DynamicViewContent,作用是标记哪些动态生成的数据项可以被抓起来拖拽。.reorderContainer(...)必须挂在承载这些子项的外层物理容器 (如List、LazyVGrid或ScrollView)上。
swift
List { // 外层大容器
ForEach(numbers) { i in // 动态数据集
RowView(i)
}
.reorderable() // 标记:"这群人可以被拖动"
}
.reorderContainer(for: Int.self) { diff in // 接收:"在这里发生碰撞,并以此更新数据"
diff.apply(to: &numbers)
}
一句话口诀:
.reorderable()标记谁能动 ,.reorderContainer(...)决定怎么变。

第七章:从单擂台到多宇宙:挑战"力之大会"
如果你的 App 里只有这十张卡片在自己家里拖来拖去,那只能算"天下第一武道会"。
但如果我们要支持更复杂的场景:
- 任务看板:任务卡片要在"待办"、"进行中"、"已完成"三个泳道(Section)之间自由拖动;
- 星球转移:把孙悟空从"地球"拖到"界王星"的列表里。
这时候,我们就需要召唤大天官的"多宇宙重排模式":
swift
// 1. 在 ForEach 里,通过 collectionID 表明当前子项属于哪个阵营/宇宙
ForEach(universe.fighters) { fighter in
FighterRow(fighter)
}
.reorderable(collectionID: universe.id) // 绑定阵营 ID

swift
// 2. 外部容器指定 Item 的类型以及 Section (Collection) 的 ID 类型
.reorderContainer(for: Fighter.self, in: Universe.ID.self) { diff in
// 此时的 diff 不仅包含被拖拽的战士,还能分辨出他们是从哪个宇宙跨越到哪个宇宙的!
model.applyCrossUniverseDifference(diff)
}
第八章:收招:当悟空放下龟派气功,SwiftUI 也放下了下标思维
在整篇文章的最后,让我们用一张简单的表来复盘这场技术革命:
| 维度 | 过去(iOS 20 之前) | 现代(WWDC26 / iOS 20 时代) |
|---|---|---|
| 支持的布局 | 几乎仅限 List |
List、VStack/HStack、LazyVGrid、自定义 Layout |
| 拖拽标识 | 系统把手(EditMode)/ 手写 DragGesture | 优雅声明式的 .reorderable() |
| 数据同步机制 | 基于不稳定的位置下标 IndexSet |
基于稳定实体标识的 ReorderDifference |
这正如悟空在力量大会上不再一味追求爆气,而是追求卸去多余负担、顺应身体本能的"自在极意功"一样------SwiftUI 也在逐渐卸去繁琐的手势胶水代码。
它让我们专注于声明最本质的逻辑:
- 把战士交出去(
.reorderable()); - 把规则定下来(
.reorderContainer); - 剩下的,放心地让 SwiftUI 去替我们安排。

下一次,当你要写一个拖动排序列表时,先别急着手搓手势。试着像布尔玛一样喝杯咖啡,气定神闲地写下这两行新招式,然后优雅地对系统说一句:
"剩下的,你先出一招吧!"
感谢宝子们的观赏,再会吧!8-)