【问题标题】:IndexSet referring to index of the section instead of the rowIndexSet 指的是节的索引而不是行的索引
【发布时间】:2019-10-29 10:41:00
【问题描述】:

我想通过使用 .onDelete 修饰符在 SwiftUI Section 上实现滑动删除功能。问题是它总是删除列表中的第一项。

我的视图有一个列表,其中包含使用ForEach 创建的动态部分。

struct SetListView : View {

    var setlist: Setlist
    var body : some View {

        List { 
            ForEach(setlist.sets) { 
                 SetSection(number: $0.id, songs: $0.songs) 
            }
        }
        .listStyle(.grouped)
    }
}

在每个部分中,还有另一个ForEach 用于创建动态行:

private struct SetSection : View {

    var number: Int
    @State var songs: [Song]

    var body : some View {
        Section (header: Text("Set \(number)"), footer: Spacer()) {
            ForEach(songs) { song in
                SongRow(song: song)
            }
            .onDelete { index in
                self.songs.remove(at: index.first!)
            }
        }
    }
}

在调试时,我发现IndexSet 指的是当前部分而不是行。所以当从第一部分删除项目时,总是第一个项目被删除(因为第一部分的索引是 0)。

这是 SwiftUI 中的错误吗?

如果没有,那我如何获得该行的索引?

【问题讨论】:

  • 这肯定是你的数据结构有问题。确保在删除特定歌曲时更新集
  • 我不同意。问题不是在删除歌曲之后发生,而是在删除之前。我认为我的数据结构与传递给闭包的 IndexSet 没有任何关系。
  • 我认为这是一个错误,或者至少是一个意外行为。如果您有多个 ForEach () {}.onDelete () 您将获得 ForEach 在所有 ForEach 定义中的位置

标签: swift list foreach swiftui


【解决方案1】:

我遇到了完全相同的问题。事实证明,SwiftUI 的(当前?)实现不识别嵌套列表。这意味着您的List 中的每个SetSection 都被解释为单行,即使您在其中有一个ForEach 和实际的SongRows。因此,IndexSet (index.first!) 总是返回零。

我还注意到,即使是扁平的层次结构,例如..

List {
   Section {
       ForEach(...) {
          ...
       }
   }
   Section {
       ForEach(...) {
          ...
       }
   }
}

..各个行不能在部分之间移动。当直接使用两个ForEach 时也是如此,即没有Section 包装器。

我们可能应该为每个现象提交一份报告。

【讨论】:

  • 根据我的测试,我同意这是一个错误。遗憾的是,它并未在 beta 3 中修复。
  • 这在 beta 4 中也没有修复,这让我觉得我错过了一些东西。我看到了一些 references to remove(atOffsets:) being added in beta 4,但考虑到 IndexSet 仍然有错误的值,我看不出这是如何解决问题的。
  • 谢天谢地,我找到了一个可行的解决方案(作为新答案发布)。
  • 我有同样的问题,有人找到解决方案吗?
  • @garrettmurray 您的解决方案适用于删除,但我也希望能够在不同部分之间移动行。我很好奇是否有人对此有解决方案,但我认为这是目前 SwiftUI 的限制。
【解决方案2】:

简单来说,解决这个问题的方法是将该部分传递给您的删除方法:

  1. 在您的源数据上采用RandomAccessCollection
  2. 在您的外部ForEach 中绑定该部分,然后在您的内部ForEach 中使用它,并将其传递给您的删除方法:
List {
  ForEach(someGroups.indices) { section in
    bind(self.someGroups[section]) { someGroup in
      Section(header: Text(someGroup.displayName)) {
        ForEach(someGroup.numbers) { number in
          Text("\(number)")
        }
        .onDelete { self.delete(at: $0, in: section) }
      }
    }
  }
}

func delete(at offsets: IndexSet, in section: Int) {
  print("\(section), \(offsets.first!)")
}

一个完整的、人为的工作示例

(Also available in Gist form for convenience):

import SwiftUI

func bind<Value, Answer>(_ value: Value, to answer: (Value) -> Answer) -> Answer { answer(value) }

struct Example: View {

  struct SomeGroup: Identifiable, RandomAccessCollection {
    typealias Indices = CountableRange<Int>
    public typealias Index = Int;
    var id: Int
    var displayName: String
    var numbers: [Int]

    public var endIndex: Index {
      return numbers.count - 1
    }

    public var startIndex: Index {
      return 0
    }

    public subscript(position: Int) -> Int {
      get { return numbers[position] }
      set { numbers[position] = newValue }
    }

  }

  var someGroups: [SomeGroup] = {
    return [
      SomeGroup(id: 0, displayName: "First", numbers: [1, 2, 3, 4]),
      SomeGroup(id: 1, displayName: "Second", numbers: [1, 3, 5, 7])
    ]
  }()

  var body: some View {
    List {
      ForEach(someGroups.indices) { section in
        bind(self.someGroups[section]) { someGroup in
          Section(header: Text(someGroup.displayName)) {
            ForEach(someGroup.numbers) { number in
              Text("\(number)")
            }
            .onDelete { self.delete(at: $0, in: section) }
          }
        }
      }
    }
    .listStyle(.grouped)
  }

  func delete(at offsets: IndexSet, in section: Int) {
    print("\(section), \(offsets.first!)")
  }

}

非常感谢@rob-mayoff,他通过 Twitter 为我指明了这个解决方案的正确方向!

【讨论】:

  • 感谢您对如何将特定部分传递给删除方法的见解。我的部分已经是使用 sectionNameKeyPath 的 NSFetchedResultsController 中的 NSFetchedResultsSectionInfo 数组。非常优雅。
【解决方案3】:

它似乎在 Xcode 12.5 上运行良好

我是这样使用的:

struct Sections: View {
    var items: [SomeData]
    private var sections: [Date: [SomeData]] {
        Dictionary(grouping: items, by: { $0.date })
    }
    private var headers: [Date] {
        sections.map({ $0.key }).sorted().reversed()
    }

    var body: some View {
        List {
            ForEach(headers, id: \.self) { date in
                Section(header: Text(date.friendly) {
                    AList(items: sections[date]!)
                }
            }
        }
    }
}

struct AList: View {
    var items: [SomeData]
    var body: some View {
        ForEach(items) { data in
            ...
        }
        .onDelete(perform: delete)
    }

    private func delete(at offsets: IndexSet) {
        // You can use `items.remove(atOffsets: offsets)`
        for offset in offsets {
            let data = items[offset]
            print("\(data)") 
    // You can check here that this is the item that you want to remove and then you need to remove it from your data source. 
    // I'm using Realm and @Published vars that works fine, you should adapt to your logic.
        }
    }
}

【讨论】:

  • 感谢您在删除之前如何进入该项目的额外步骤。
猜你喜欢
  • 2020-05-09
  • 1970-01-01
  • 2012-11-21
  • 2011-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-17
  • 1970-01-01
相关资源
最近更新 更多