在列表的各节之间移动项目时,SwiftUI的异常行为

时间:2019-09-05 10:40:56

标签: swift swiftui

所以我一直在尝试使用swiftUI制作组件,该组件允许您在各节之间的List中移动项目。

我准备了一个包含两个部分的示例:“第一列表”和“第二列表”。每当您点击一个项目时,它都会交换部分。这是屏幕截图:

enter image description here

当我点击“第一列表:1”时,它会正确移至第二部分:

enter image description here

但是,由于我在各节中为元素命名的方式(请参见下面的代码),其名称现在应更改为“第二列表:1”。所以这很奇怪。但它变得陌生:

当我现在在第二部分中点击“第一列表:1”时,会发生这种情况:

enter image description here

它不能正确换回。它只是被复制了,但是这次复制的名称实际上是正确的。

考虑下面的代码,我不知道这是怎么可能的。似乎swiftUI以某种方式重用了该项目,即使它重新渲染了视图?似乎也重用了.onTapGesture闭包,因为实际上从未调用过将项目放回第一部分的方法。

知道这里发生了什么吗?以下是该问题的完整示例:

import SwiftUI
import Combine

struct TestView: View {
    @ObservedObject var viewModel: ViewModel

    class ViewModel: ObservableObject {
        let objectWillChange = PassthroughSubject<ViewModel,Never>()

        public enum List {
            case first
            case second
        }

        public var first: [Int] = []
        public var second: [Int] = []

        public func swap(elementWithIdentifier identifier: Int, from list: List) {
            switch list {
            case .first:
                self.first.removeAll(where: {$0 == identifier})
                self.second.append(identifier)
            case .second:
                print("Called")
                self.second.removeAll(where: {$0 == identifier})
                self.first.append(identifier)
            }

            self.objectWillChange.send(self)
        }

        init(first: [Int]) {
            self.first = first
        }
    }

    var body: some View {
        NavigationView {
            List {
                Section(header: Text("First List")) {
                    ForEach(self.viewModel.first, id: \.self) { id in
                        Text("First List: \(id)")
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .onTapGesture {
                                self.viewModel.swap(elementWithIdentifier: id, from: .first)
                        }
                    }
                }

                Section(header: Text("First List")) {
                    ForEach(self.viewModel.second, id: \.self) { id in
                        Text("Second List: \(id)")
                            .onTapGesture {
                                self.viewModel.swap(elementWithIdentifier: id, from: .second)
                        }
                    }
                }
            }
            .listStyle(GroupedListStyle())
            .navigationBarTitle(Text("Testing"))
        }.environment(\.editMode, .constant(EditMode.active))
    }
}

struct TestView_Preview: PreviewProvider {
    static var previews: some View {
        TestView(viewModel: TestView.ViewModel(first: [1, 2, 3, 4, 5]))
    }
}

3 个答案:

答案 0 :(得分:0)

我解决此问题的唯一方法是通过向列表添加随机ID来防止列表差异。不过,这会删除动画,因此正在寻找更好的解决方案

id="notes"

删除这些部分也可以解决此问题,但这也不是有效的解决方案

答案 1 :(得分:0)

我发现自己处于类似情况,并且找到了解决该问题的更优雅的解决方法。我相信问题在于iOS13。在iOS14中,该问题不再存在。下面详细介绍了一个可在iOS13和iOS14上运行的简单解决方案。

尝试一下:

extension Int {
    var id:UUID {
        return UUID()
    }
}

,然后在您的ForEach参考\.id\.self.id中而不是\.self,即在您的两个部分中都这样:

ForEach(self.viewModel.first, id: \.id) { id in
    Text("First List: \(id)")
       .onTapGesture {
            self.viewModel.swap(elementWithIdentifier: id, from: .first)
       }
}

这会使事情正常。但是,摆弄时我确实发现了以下问题:

  1. iOS14中几乎不存在动画。可以解决这个问题。
  2. 在iOS13中,.listStyle(GroupedListStyle())动画看起来很奇怪。删除它,动画效果会好很多。
  3. 我尚未在大型列表中测试过此解决方案。因此,请注意可能的性能问题。对于较小的列表,它可以工作。

再一次,这是一种解决方法,但我认为Apple仍在解决SwiftUI中的问题。

更新

PS,如果您在iOS14中使用任何onDeleteonMove修饰符,则会将动画添加到列表中,从而导致异常行为。我发现使用\.self适用于iOS14,\.self.id适用于iOS13。代码不是很漂亮,因为您很可能会在代码中插入#available(iOS 14.0, *)个代码。但这有效。

答案 2 :(得分:0)

我不知道为什么,但似乎您的 swap 方法在您添加的第一个对象上做了一些奇怪的事情,因为如果第二个有效,那么您可能丢失了一些实例。

顺便说一句,每次在每个列表中添加一个新对象时,是否都需要 removeAll?

opacity

也许你的问题出在这个函数上,一切看起来都很棒。