SwiftUI List含.moveDisabled(true)行时拖拽目标缺失问题问询
SwiftUI List 固定行拖拽异常问题分析与解决方案
问题描述
实现了带有固定行(设置.moveDisabled(true),红色背景标识)的可编辑SwiftUI List,出现以下拖拽异常:
- 拖拽可移动行跨越固定行时,固定行后的位置无法作为拖拽目标;
- 无法将可移动行移入相邻固定行之间;
- 上述问题在iOS 16.4、iOS 17.0 beta2版本中均存在,仅当不跨越固定行时,拖拽目标位置可正常使用。
复现代码
import SwiftUI class Model: ObservableObject { struct Item: Identifiable { var id = UUID() var name: String var isFixed: Bool } @Published var items: [Item] = [ Item(name: "Fixed 1", isFixed: true), Item(name: "Movable 1", isFixed: false), Item(name: "Movable 2", isFixed: false), Item(name: "Fixed 2", isFixed: true), Item(name: "Movable 3", isFixed: false), Item(name: "Movable 4", isFixed: false) ] func move(from source: IndexSet, to destination: Int) { items.move(fromOffsets: source, toOffset: destination) } } struct ContentView: View { @StateObject private var model = Model() var body: some View { NavigationView { List { ForEach(model.items) { item in if item.isFixed { Text(item.name) .listRowBackground(Color.red) .moveDisabled(true) } else { Text(item.name) } } .onMove(perform: model.move) } .listStyle(.grouped) .toolbar { EditButton() } } } } struct MyPreviewProvider_Previews: PreviewProvider { static var previews: some View { ContentView() } }
问题定性
- 非操作失误:问题是由SwiftUI List内部对
.moveDisabled的处理逻辑缺陷导致,与用户操作无关; - 属于Apple Bug:在多个iOS版本中均存在,核心问题是List在计算拖拽目标位置时,错误地将固定行附近的合法位置判定为不可用,尤其在跨固定行拖拽时表现明显。
解决方案(Workaround)
通过手动修正拖拽目标位置的索引,绕过SwiftUI的错误判定逻辑。修改Model中的move方法,重新计算合法的目标索引:
func move(from source: IndexSet, to destination: Int) { // 获取所有可移动行在原数组中的索引 let movableIndices = items.indices.filter { !items[$0].isFixed } guard let sourceIndex = source.first, // 找到拖拽源在可移动行序列中的位置 let movableSourcePos = movableIndices.firstIndex(of: sourceIndex) else { return } // 计算目标位置在可移动行序列中的相对位置(排除固定行的数量) var movableDestPos = destination - items[0..<destination].filter { $0.isFixed }.count // 处理拖拽源在目标位置之前的情况,避免索引偏移错误 if sourceIndex < destination { movableDestPos -= 1 } // 确保目标位置在合法范围内 movableDestPos = max(0, min(movableDestPos, movableIndices.count)) // 将可移动行的相对位置转换回原数组的索引 let targetIndex: Int if movableDestPos == movableIndices.count { targetIndex = items.endIndex } else { targetIndex = movableIndices[movableDestPos] } // 执行移动操作 items.move(fromOffsets: source, toOffset: targetIndex) }
此方法通过分离可移动行的索引逻辑,手动映射拖拽目标的真实位置,实现了可移动行向任意合法位置(包括固定行后、相邻固定行之间)的拖拽。
内容的提问来源于stack exchange,提问作者Gerd Castan
相关产品推荐
相关产品推荐

