You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI如何检测列表项跨Section移动并阻止报错?

解决SwiftUI列表跨分区拖拽排序的问题

问题描述

我有两个列表分别存放数字和字母,长按列表行可拖拽重排序,但SwiftUI没有阻止列表项在不同ForEach间移动,将字母拖拽到数字分区会引发错误。示例代码如下:

import SwiftUI

struct ContentView: View {
    @State private var alphabets = ["a", "b", "c"]
    @State private var numbers = ["1", "2", "3"]
    
    var body: some View {
        NavigationView {
            List {
                Section("Alphabets") {
                    ForEach(alphabets, id: \.self) { alphabet in
                        Text(alphabet)
                    }
                    .onMove { fromOffsets, toOffset in
                        alphabets.move(fromOffsets: fromOffsets, toOffset: toOffset)
                    }
                }
                Section("numbers") {
                    ForEach(numbers, id: \.self) { number in
                        Text(number)
                    }
                    .onMove { fromOffsets, toOffset in
                        alphabets.move(fromOffsets: fromOffsets, toOffset: toOffset)
                    }
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

我记得iOS 16.0之前似乎不会出现此问题,希望实现如下逻辑:

.onMove { fromOffsets, toOffset in
    if destinationSectionId == thisSectionId {
        alphabets.move(fromOffsets: fromOffsets, toOffset: toOffset)
    } else {
        do nothing
        display an animation to move the item back
    }
}

解决方案

可以通过判断拖拽目标的偏移范围,限制仅允许同分区内的拖拽排序,同时触发回弹动画,具体实现如下:

方法1:基于onMove回调的范围判断(适配iOS 16+)

在onMove回调中计算当前分区的偏移范围,判断目标位置是否在当前分区内,不在则触发回弹:

import SwiftUI

struct ContentView: View {
    @State private var alphabets = ["a", "b", "c"]
    @State private var numbers = ["1", "2", "3"]
    
    var body: some View {
        NavigationStack { // iOS 16+推荐使用NavigationStack替代NavigationView
            List {
                Section("Alphabets") {
                    ForEach(alphabets, id: \.self) { alphabet in
                        Text(alphabet)
                    }
                    .onMove { fromOffsets, toOffset in
                        // Alphabets分区的偏移范围:从0到列表元素数量
                        let sectionStart = 0
                        let sectionEnd = alphabets.count
                        if toOffset >= sectionStart && toOffset <= sectionEnd {
                            alphabets.move(fromOffsets: fromOffsets, toOffset: toOffset)
                        } else {
                            // 触发默认回弹动画
                            withAnimation {}
                        }
                    }
                }
                Section("Numbers") {
                    ForEach(numbers, id: \.self) { number in
                        Text(number)
                    }
                    .onMove { fromOffsets, toOffset in
                        // Numbers分区的起始偏移 = Alphabets元素数 + 1(Section标题占位)
                        let sectionStart = alphabets.count + 1
                        let sectionEnd = sectionStart + numbers.count
                        if toOffset >= sectionStart && toOffset <= sectionEnd {
                            // 目标偏移需要转换为当前分区内的相对偏移
                            numbers.move(fromOffsets: fromOffsets, toOffset: toOffset - sectionStart)
                        } else {
                            withAnimation {}
                        }
                    }
                }
            }
            .toolbar {
                EditButton()
            }
        }
    }
}

关键说明

  • iOS 16之后,SwiftUI默认允许跨分区拖拽,因此需要手动判断目标偏移是否在当前分区范围内。
  • 当拖拽到其他分区时,空的withAnimation {}会触发SwiftUI的默认回弹动画,将元素放回原位置。
  • 计算分区偏移时要注意:每个Section的标题会占用一个偏移位置,所以Numbers分区的起始偏移需要加上Alphabets的元素数量和1个标题占位。

内容的提问来源于stack exchange,提问作者Josh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 01:15:35