序幕从龟仙人的训练场到 SwiftUI 的重排宇宙布尔玛“喂悟空贝吉塔还有那边那个光头克林天下第一武道会的登记表被你们弄乱了谁准你们把弗利萨排在第一位的”孙悟空挠头笑“嘿嘿因为弗利萨看起来很强嘛我想先跟他交手”贝吉塔双手抱胸冷哼“哼卡卡罗特。在战斗中出场顺序就是一切。我怎么可能排在雅木茶后面”在过去如果我们想在 SwiftUI 的List里调整这群宇宙级战士的出场顺序通常只有两条路要么让用户点击“编辑”按钮小心翼翼地拖动系统给的灰色小把手要么就得像比克Piccolo训练悟饭那样一拳一脚地去手搓DragGesture物理碰撞逻辑。好在WWDC26iOS 27为 SwiftUI 带来了更直接的“合体必杀技”.reorderable()与.reorderContainer(...)。今天我们就借着这个龙珠测试项目把这套全新招式从入门到实战彻底讲透。第一章天下第一武道会的出场顺序为什么不能只看 UI在ContentView.swift里武道会的前台接待处非常清爽一个NavigationStack两个分会场入口。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里的基本演示。structListReorderDemoView:View{StateprivatevarnumbersArray(0...9)varbody:someView{List{ForEach(numbers){iinVStack(alignment:.leading,spacing:10){HStack(spacing:18){Image(B\(i1)).resizable().scaledToFill().frame(width:112,height:112).clipShape(RoundedRectangle(cornerRadius:10))Text(\(i)).font(.system(size:88))}}}.reorderable()// 标记这里的每一行都是可拖动的战士}.reorderContainer(for:Int.self){diffin// 告诉容器这里是 Int 选手的专属擂台diff.apply(to:numbers)// 裁判宣布最终结果直接修改数据源}}}龟仙人拄着拐杖戴着墨镜“咳咳悟空啊你这个代码能跑通全亏了后山那段悄悄给Int补上的‘外挂’”extensionInt:retroactiveIdentifiable{publicvarid:Int{self}}因为.reorderContainer(for:)内部有着严格的家规被重排的类型必须符合Identifiable协议以便系统能够跨线程安全地识别每个元素。虽然我们在测试时可以用retroactive让标准库的Int直接用自身当id但在真正的生产项目比如卡普空或万代的游戏开发中贝吉塔是绝对不允许这种偷懒行为的。更标准的“赛亚人写法”应该是定义明确的模型structFighter:Identifiable,Sendable{letid:Int// 唯一的战斗员编号varname:String// 姓名比如“孙悟空”varimageName: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而是自己手写了一个优雅的扩展。比克双臂抱胸头蓬迎风飘扬“悟饭别光看着仔细看这段逻辑这才是真正的气功运行路线”importCollections// 引入官方 swift-collections 库extensionReorderDifferencewhereCollectionIDReorderableSingleCollectionIdentifier{funcapply(to values:inout[someIdentifiableItemID]){// 1. 先用当前的战士数组生成一个以 ID 为 Key、战士本体为 Value 的有序字典vardictionaryOrderedDictionary(uniqueKeys:values.map(\.id),values:values)// 2. 找到目标落脚点的 Offset 到底在当前字典的哪个位置letdestinationOffset:Int?switchdestination.position{case.before(letdestination):dictionary.keys.firstIndex(of:destination)case.end:nil}// 3. 将被拖拽的那批战士sources整体迁徙到目标落脚点dictionary.move(keys:sources,to:destinationOffset??values.endIndex)// 4. 将调整完毕的字典重新写回原数组valuesdictionary.values.elements}}比克老师解释道这里引入OrderedDictionary主要有两个原因像普通字典一样通过 ID 查找能极速响应ReorderDifference.sources这种基于“身份Identity”的移动指令。像普通数组一样保持顺序方便完美将排序结果呈现在 UI 上。这正是“有序 Key 检索”的完美结合也是我们依赖swift-collections的底气所在。第五章龙珠雷达展开LazyVGrid 也能用同一套重排模型接着我们把战场转移到LazyVGridReorderDemoView.swift。structLazyVGridReorderDemoView:View{StateprivatevarphotosArray(0...9)privateletcolumns[GridItem(.adaptive(minimum:150),spacing:16)]varbody:someView{ScrollView{LazyVGrid(columns:columns,spacing:16){ForEach(photos){photoIndexinVStack(alignment:.leading,spacing:8){Image(B\(photoIndex1)).resizable().scaledToFill().frame(minWidth:0,maxWidth:.infinity).aspectRatio(1,contentMode:.fill).clipShape(RoundedRectangle(cornerRadius:10))Text(图片\(photoIndex1))}}.reorderable()// 瞧也是只要在这里挂载即可}.reorderContainer(for:Int.self){diffindiff.apply(to:photos)// 同样的一招制敌}}}}布尔玛得意地晃了晃手中的雷达“看吧在我的雷达网格里重排的代码几乎一字未改这就是 WWDC26 重排 API 的最伟大之处——它不再是List的专属特权”过去我们要写网格重排手势检测、计算坐标、处理插值代码写得像那美克星的历史一样冗长。现在不管是单列列表还是动态网格底层都共享着同一套ReorderDifference模型。第六章界王星的修炼禁忌.reorderable()应该挂在哪里北界王抱着猩猩巴布鲁斯一脸严肃“听好了悟空如果你把气功的姿势摆错了不仅打不中敌人可能还会把界王星给炸了这两个 Modifier 的位置绝对不能放反”我们必须牢记.reorderable()必须挂在ForEach的后面。它修饰的是DynamicViewContent作用是标记哪些动态生成的数据项可以被抓起来拖拽。.reorderContainer(...)必须挂在承载这些子项的外层物理容器如List、LazyVGrid或ScrollView上。List{// 外层大容器ForEach(numbers){iin// 动态数据集RowView(i)}.reorderable()// 标记“这群人可以被拖动”}.reorderContainer(for:Int.self){diffin// 接收“在这里发生碰撞并以此更新数据”diff.apply(to:numbers)}一句话口诀.reorderable()标记谁能动.reorderContainer(...)决定怎么变。第七章从单擂台到多宇宙挑战“力之大会”如果你的 App 里只有这十张卡片在自己家里拖来拖去那只能算“天下第一武道会”。但如果我们要支持更复杂的场景任务看板任务卡片要在“待办”、“进行中”、“已完成”三个泳道Section之间自由拖动星球转移把孙悟空从“地球”拖到“界王星”的列表里。这时候我们就需要召唤大天官的“多宇宙重排模式”// 1. 在 ForEach 里通过 collectionID 表明当前子项属于哪个阵营/宇宙ForEach(universe.fighters){fighterinFighterRow(fighter)}.reorderable(collectionID:universe.id)// 绑定阵营 ID// 2. 外部容器指定 Item 的类型以及 Section (Collection) 的 ID 类型.reorderContainer(for:Fighter.self,in:Universe.ID.self){diffin// 此时的 diff 不仅包含被拖拽的战士还能分辨出他们是从哪个宇宙跨越到哪个宇宙的model.applyCrossUniverseDifference(diff)}第八章收招当悟空放下龟派气功SwiftUI 也放下了下标思维在整篇文章的最后让我们用一张简单的表来复盘这场技术革命维度过去iOS 20 之前现代WWDC26 / iOS 20 时代支持的布局几乎仅限ListList、VStack/HStack、LazyVGrid、自定义 Layout拖拽标识系统把手EditMode/ 手写 DragGesture优雅声明式的.reorderable()数据同步机制基于不稳定的位置下标IndexSet基于稳定实体标识的ReorderDifference这正如悟空在力量大会上不再一味追求爆气而是追求卸去多余负担、顺应身体本能的“自在极意功”一样——SwiftUI 也在逐渐卸去繁琐的手势胶水代码。它让我们专注于声明最本质的逻辑把战士交出去.reorderable()把规则定下来.reorderContainer剩下的放心地让 SwiftUI 去替我们安排。下一次当你要写一个拖动排序列表时先别急着手搓手势。试着像布尔玛一样喝杯咖啡气定神闲地写下这两行新招式然后优雅地对系统说一句“剩下的你先出一招吧”感谢宝子们的观赏再会吧
WWDC26 全网首发:SwiftUI 8 “可重排序“操作符深度解析
发布时间:2026/6/10 23:15:14
序幕从龟仙人的训练场到 SwiftUI 的重排宇宙布尔玛“喂悟空贝吉塔还有那边那个光头克林天下第一武道会的登记表被你们弄乱了谁准你们把弗利萨排在第一位的”孙悟空挠头笑“嘿嘿因为弗利萨看起来很强嘛我想先跟他交手”贝吉塔双手抱胸冷哼“哼卡卡罗特。在战斗中出场顺序就是一切。我怎么可能排在雅木茶后面”在过去如果我们想在 SwiftUI 的List里调整这群宇宙级战士的出场顺序通常只有两条路要么让用户点击“编辑”按钮小心翼翼地拖动系统给的灰色小把手要么就得像比克Piccolo训练悟饭那样一拳一脚地去手搓DragGesture物理碰撞逻辑。好在WWDC26iOS 27为 SwiftUI 带来了更直接的“合体必杀技”.reorderable()与.reorderContainer(...)。今天我们就借着这个龙珠测试项目把这套全新招式从入门到实战彻底讲透。第一章天下第一武道会的出场顺序为什么不能只看 UI在ContentView.swift里武道会的前台接待处非常清爽一个NavigationStack两个分会场入口。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里的基本演示。structListReorderDemoView:View{StateprivatevarnumbersArray(0...9)varbody:someView{List{ForEach(numbers){iinVStack(alignment:.leading,spacing:10){HStack(spacing:18){Image(B\(i1)).resizable().scaledToFill().frame(width:112,height:112).clipShape(RoundedRectangle(cornerRadius:10))Text(\(i)).font(.system(size:88))}}}.reorderable()// 标记这里的每一行都是可拖动的战士}.reorderContainer(for:Int.self){diffin// 告诉容器这里是 Int 选手的专属擂台diff.apply(to:numbers)// 裁判宣布最终结果直接修改数据源}}}龟仙人拄着拐杖戴着墨镜“咳咳悟空啊你这个代码能跑通全亏了后山那段悄悄给Int补上的‘外挂’”extensionInt:retroactiveIdentifiable{publicvarid:Int{self}}因为.reorderContainer(for:)内部有着严格的家规被重排的类型必须符合Identifiable协议以便系统能够跨线程安全地识别每个元素。虽然我们在测试时可以用retroactive让标准库的Int直接用自身当id但在真正的生产项目比如卡普空或万代的游戏开发中贝吉塔是绝对不允许这种偷懒行为的。更标准的“赛亚人写法”应该是定义明确的模型structFighter:Identifiable,Sendable{letid:Int// 唯一的战斗员编号varname:String// 姓名比如“孙悟空”varimageName: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而是自己手写了一个优雅的扩展。比克双臂抱胸头蓬迎风飘扬“悟饭别光看着仔细看这段逻辑这才是真正的气功运行路线”importCollections// 引入官方 swift-collections 库extensionReorderDifferencewhereCollectionIDReorderableSingleCollectionIdentifier{funcapply(to values:inout[someIdentifiableItemID]){// 1. 先用当前的战士数组生成一个以 ID 为 Key、战士本体为 Value 的有序字典vardictionaryOrderedDictionary(uniqueKeys:values.map(\.id),values:values)// 2. 找到目标落脚点的 Offset 到底在当前字典的哪个位置letdestinationOffset:Int?switchdestination.position{case.before(letdestination):dictionary.keys.firstIndex(of:destination)case.end:nil}// 3. 将被拖拽的那批战士sources整体迁徙到目标落脚点dictionary.move(keys:sources,to:destinationOffset??values.endIndex)// 4. 将调整完毕的字典重新写回原数组valuesdictionary.values.elements}}比克老师解释道这里引入OrderedDictionary主要有两个原因像普通字典一样通过 ID 查找能极速响应ReorderDifference.sources这种基于“身份Identity”的移动指令。像普通数组一样保持顺序方便完美将排序结果呈现在 UI 上。这正是“有序 Key 检索”的完美结合也是我们依赖swift-collections的底气所在。第五章龙珠雷达展开LazyVGrid 也能用同一套重排模型接着我们把战场转移到LazyVGridReorderDemoView.swift。structLazyVGridReorderDemoView:View{StateprivatevarphotosArray(0...9)privateletcolumns[GridItem(.adaptive(minimum:150),spacing:16)]varbody:someView{ScrollView{LazyVGrid(columns:columns,spacing:16){ForEach(photos){photoIndexinVStack(alignment:.leading,spacing:8){Image(B\(photoIndex1)).resizable().scaledToFill().frame(minWidth:0,maxWidth:.infinity).aspectRatio(1,contentMode:.fill).clipShape(RoundedRectangle(cornerRadius:10))Text(图片\(photoIndex1))}}.reorderable()// 瞧也是只要在这里挂载即可}.reorderContainer(for:Int.self){diffindiff.apply(to:photos)// 同样的一招制敌}}}}布尔玛得意地晃了晃手中的雷达“看吧在我的雷达网格里重排的代码几乎一字未改这就是 WWDC26 重排 API 的最伟大之处——它不再是List的专属特权”过去我们要写网格重排手势检测、计算坐标、处理插值代码写得像那美克星的历史一样冗长。现在不管是单列列表还是动态网格底层都共享着同一套ReorderDifference模型。第六章界王星的修炼禁忌.reorderable()应该挂在哪里北界王抱着猩猩巴布鲁斯一脸严肃“听好了悟空如果你把气功的姿势摆错了不仅打不中敌人可能还会把界王星给炸了这两个 Modifier 的位置绝对不能放反”我们必须牢记.reorderable()必须挂在ForEach的后面。它修饰的是DynamicViewContent作用是标记哪些动态生成的数据项可以被抓起来拖拽。.reorderContainer(...)必须挂在承载这些子项的外层物理容器如List、LazyVGrid或ScrollView上。List{// 外层大容器ForEach(numbers){iin// 动态数据集RowView(i)}.reorderable()// 标记“这群人可以被拖动”}.reorderContainer(for:Int.self){diffin// 接收“在这里发生碰撞并以此更新数据”diff.apply(to:numbers)}一句话口诀.reorderable()标记谁能动.reorderContainer(...)决定怎么变。第七章从单擂台到多宇宙挑战“力之大会”如果你的 App 里只有这十张卡片在自己家里拖来拖去那只能算“天下第一武道会”。但如果我们要支持更复杂的场景任务看板任务卡片要在“待办”、“进行中”、“已完成”三个泳道Section之间自由拖动星球转移把孙悟空从“地球”拖到“界王星”的列表里。这时候我们就需要召唤大天官的“多宇宙重排模式”// 1. 在 ForEach 里通过 collectionID 表明当前子项属于哪个阵营/宇宙ForEach(universe.fighters){fighterinFighterRow(fighter)}.reorderable(collectionID:universe.id)// 绑定阵营 ID// 2. 外部容器指定 Item 的类型以及 Section (Collection) 的 ID 类型.reorderContainer(for:Fighter.self,in:Universe.ID.self){diffin// 此时的 diff 不仅包含被拖拽的战士还能分辨出他们是从哪个宇宙跨越到哪个宇宙的model.applyCrossUniverseDifference(diff)}第八章收招当悟空放下龟派气功SwiftUI 也放下了下标思维在整篇文章的最后让我们用一张简单的表来复盘这场技术革命维度过去iOS 20 之前现代WWDC26 / iOS 20 时代支持的布局几乎仅限ListList、VStack/HStack、LazyVGrid、自定义 Layout拖拽标识系统把手EditMode/ 手写 DragGesture优雅声明式的.reorderable()数据同步机制基于不稳定的位置下标IndexSet基于稳定实体标识的ReorderDifference这正如悟空在力量大会上不再一味追求爆气而是追求卸去多余负担、顺应身体本能的“自在极意功”一样——SwiftUI 也在逐渐卸去繁琐的手势胶水代码。它让我们专注于声明最本质的逻辑把战士交出去.reorderable()把规则定下来.reorderContainer剩下的放心地让 SwiftUI 去替我们安排。下一次当你要写一个拖动排序列表时先别急着手搓手势。试着像布尔玛一样喝杯咖啡气定神闲地写下这两行新招式然后优雅地对系统说一句“剩下的你先出一招吧”感谢宝子们的观赏再会吧