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

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()
    }
}

问题定性

  1. 非操作失误:问题是由SwiftUI List内部对.moveDisabled的处理逻辑缺陷导致,与用户操作无关;
  2. 属于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 00:52:35