WWDC26 全网首发:SwiftUI 8 “可重排序“操作符深度解析

序幕:从龟仙人的训练场到 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 这次的设计非常聪明,它把重排拆成了两步:"

  1. 谁能被拖着走? ------ 挂上属性 .reorderable()
  2. 落地后数据怎么变? ------ 交给外层容器的 .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 主要有两个原因:

  1. 像普通字典一样通过 ID 查找 :能极速响应 ReorderDifference.sources 这种基于"身份(Identity)"的移动指令。
  2. 像普通数组一样保持顺序:方便完美将排序结果呈现在 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(...) 必须挂在承载这些子项的外层物理容器 (如 ListLazyVGridScrollView)上。
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 ListVStack/HStackLazyVGrid、自定义 Layout
拖拽标识 系统把手(EditMode)/ 手写 DragGesture 优雅声明式的 .reorderable()
数据同步机制 基于不稳定的位置下标 IndexSet 基于稳定实体标识的 ReorderDifference

这正如悟空在力量大会上不再一味追求爆气,而是追求卸去多余负担、顺应身体本能的"自在极意功"一样------SwiftUI 也在逐渐卸去繁琐的手势胶水代码。

它让我们专注于声明最本质的逻辑:

  • 把战士交出去(.reorderable()
  • 把规则定下来(.reorderContainer
  • 剩下的,放心地让 SwiftUI 去替我们安排。

下一次,当你要写一个拖动排序列表时,先别急着手搓手势。试着像布尔玛一样喝杯咖啡,气定神闲地写下这两行新招式,然后优雅地对系统说一句:

"剩下的,你先出一招吧!"

感谢宝子们的观赏,再会吧!8-)

相关推荐
邓小乐2 小时前
Workaround: Xcode27 下载iOS 27.0 Simulator
ios·xcode
韩曙亮3 小时前
【Flutter】Flutter 中的 Android / iOS 特殊配置 ① ( 网络权限配置 | HTTP 明文传输配置 | 应用名称配置 )
android·网络·flutter·http·ios·网络权限
人月神话-Lee5 小时前
【图像处理】颜色空间——RGB之外的世界
图像处理·人工智能·ios·ai编程·swift·rgb·颜色空间
东坡肘子5 小时前
WWDC 2026 初印象:符合预期,但更务实 -- 肘子的 Swift 周报 #139
人工智能·swiftui·swift
CocoaKier6 小时前
苹果后台年龄分级填写错误,可能导致审核被拒!
ios·apple
月诸清酒6 小时前
Codex 现在能在浏览器里跑 iOS 模拟器了
ios
武子康7 小时前
调查研究-159 Apple WWDC 2026 定档 6/8-12:Siri 与 AI 升级,可能是苹果最关键的一次
人工智能·深度学习·ios·ai·chatgpt·apple·wwdc
2601_961194027 小时前
27考研资料|百度网盘|夸克网盘
android·xml·考研·ios·iphone·xcode·webview
2601_955767428 小时前
2026年iPhone17AR护眼膜推荐:悟赫德
人工智能·科技·ios·iphone·圆偏振光