【问题标题】:Why does sorted(by:) work but sort(by:) does not in ForEach loop为什么 sorted(by:) 有效但 sort(by:) 在 ForEach 循环中无效
【发布时间】:2020-10-13 14:54:03
【问题描述】:

我正在尝试以有序的方式显示自定义对象列表。 虽然这有效......

import SwiftUI

struct MyModel: Identifiable {
    let id = UUID()
    let timestamp: Date
}

struct ViewModel {
    var models = [
        MyModel(timestamp: Date().addingTimeInterval(300)),
        MyModel(timestamp: Date().addingTimeInterval(100)),
        MyModel(timestamp: Date().addingTimeInterval(500))
    ]
}

struct MyView: View {
    @State var viewModel = ViewModel()
    
    var body: some View {
        List {
            ForEach(viewModel.models.sorted(by: { (lhs, rhs) -> Bool in // will not work with `sort(by:)`
                lhs.timestamp > rhs.timestamp
            })) { model in
                Text("\(model.timestamp)")
            }
            .onDelete(perform: { indexSet in
                //delete from viewModel.models
            })
        }
    }
}

...删除时它不够用,因为我正在处理的数组没有排序(可以说只是它的可视化)。

sort(by:) 应该是我需要的,因为它“对集合进行适当的排序,使用给定的谓词作为元素之间的比较。”根据 Apple 的文档。

但是,相同的ForEach 语句不适用于sort(by:),抛出错误Cannot convert value of type '()' to expected argument type 'Range<Int>'

在这种情况下,有没有办法使用ForEach 进行就地排序和循环,还是我必须在我的ViewModel 中处理排序?

【问题讨论】:

  • 一个是变异函数,另一个返回一个新数组。如果您只想在视图模型中对数组进行一次排序
  • 好的,明白了。变异后,我可以在队列中获取变异数组吗?
  • 这里的变异意味着文档说它是被排序的原始数组,所以你已经有了它。
  • 我也是这么想的——不过,我不能用 ForEach 循环它,这在 imo 中应该是可能的。
  • @ASSeeger 为什么应该是可能的? Void 的返回类型(又名空元组,())。迭代它与for i in print("hello world") 没有什么不同:这没有任何意义。您正在寻找非变异版本,为什么不直接使用它?

标签: swift swiftui


【解决方案1】:

之所以不能在ForEach 中使用sort(by:),是因为sort(by:) 是一个返回Void 的变异函数。另一方面,sorted(by:) 返回一个新数组,您可以使用 ForEach 遍历该数组。

您可以通过在ViewModel 上创建一个单独的计算属性来解决您的问题,该属性返回已排序的models 并使用此已排序的属性调用ForEach。您只需要确保您还对已排序的数组执行删除操作。

struct ViewModel {
    var models = [
        MyModel(timestamp: Date().addingTimeInterval(300)),
        MyModel(timestamp: Date().addingTimeInterval(100)),
        MyModel(timestamp: Date().addingTimeInterval(500))
    ]

    var sortedModels: [MyModel] {
        get {
            models.sorted(by: { $0.timestamp > $1.timestamp })
        }
        set {
            models = newValue
        }
    }
}

struct MyView: View {
    @State var viewModel = ViewModel()

    var body: some View {
        List {
            ForEach(viewModel.sortedModels) { model in
                Text("\(model.timestamp)")
            }
            .onDelete(perform: { indexSet in
                for index in indexSet {
                    viewModel.sortedModels.remove(at: index)
                }
            })
        }
    }
}

【讨论】:

  • 这是一个很好的解决方案,谢谢。我想我也可以在 Collection 上写一个扩展来拥有一个sort(by:) -> [Element],我想(理论上来说)。
  • 这有点矫枉过正@LeoDabus,更实用的解决方案是在setter中排序(假设初始集合已经排序)。吸气剂应该是 O(1)
  • @ClausJørgensen 此处显示的 getter 无论如何都会在每次调用此属性时对集合进行排序。
  • @DávidPásztor 顺便说一句,如果集合已经排序,则删除元素后不需要再次排序。删除元素时我也会反转索引。
【解决方案2】:

无需创建计算属性。当您的列表出现时,您可以简单地对模型进行排序。如果模型已经排序,则无需在每次删除模型时对其进行排序。您还应该使您的模型符合可比较的协议:

import SwiftUI

struct ContentView: View {
    @State var viewModel = ViewModel()
    var body: some View {
        List {
            ForEach(viewModel.models) {
                Text("\($0.timestamp)")
            }
            .onDelete {
                $0.reversed().forEach { viewModel.models.remove(at: $0) }
            }
        }.onAppear {
            viewModel.models.sort(by: >)
        }
    }
}

struct MyModel: Identifiable {
    let id = UUID()
    let timestamp: Date
}

extension MyModel: Comparable {
    static func < (lhs: Self, rhs: Self) -> Bool {
        lhs.timestamp < rhs.timestamp
    }
}

struct ViewModel {
    var models = [
        MyModel(timestamp: Date().addingTimeInterval(300)),
        MyModel(timestamp: Date().addingTimeInterval(100)),
        MyModel(timestamp: Date().addingTimeInterval(500))
    ]
}

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

【讨论】:

  • 这实际上是一个好点!我决定在 ViewModel 中进行排序,因为我认为那是更好的地方(尝试坚持使用 MVVM 时)——但在其他情况下使用 .onAppear 可能会派上用场。
【解决方案3】:

除了其他两个答案之外,我想添加更多关于您打算如何使用 ViewModel 的信息。

本质上,您希望有一个 ViewModel,它是一个 ObservableObject 和一个 @Published 属性,用于您希望更改的值。这样你就可以在 ViewModel 中封装任何重要的逻辑。

拥有一个结构 ViewModel 没有任何意义,因为结构不是可变的。

此外,您可以在模型类型上实现Comparable,允许您将排序绑定到类型,而不是在代码中的随机位置。现在,如果您的模型已经排序,那么当您删除单个模型时,如何也不必重新排序内容。

如果你有插入,你会想在插入时对它们进行排序。

最后,您应该只对 UI 状态使用 @State 变量,而不是 (View)Model 状态。

import SwiftUI

struct MyModel: Identifiable, Equatable, Comparable {
    let id = UUID()
    let timestamp: Date

    static func < (lhs: MyModel, rhs: MyModel) -> Bool {
        return lhs.timestamp > rhs.timestamp
    }
}

class ViewModel: ObservableObject {
    @Published
    private(set) var models: [MyModel]

    init() {
        let now = Date()
        models = [
            MyModel(timestamp: now.addingTimeInterval(300)),
            MyModel(timestamp: now.addingTimeInterval(100)),
            MyModel(timestamp: now.addingTimeInterval(500))
        ].sorted()
    }

    func remove(at indicies: IndexSet) {
        indicies.forEach { index in
            models.remove(at: index)
        }
    }
}

struct ContentView: View {
    @ObservedObject
    var viewModel = ViewModel()

    var body: some View {
        List {
            ForEach(viewModel.models) { model in
                Text("\(model.timestamp)")
            }
            .onDelete(perform: { indexSet in
                viewModel.remove(at: indexSet)
            })
        }
    }
}

【讨论】:

  • 感谢您指出我的 ViewModel 应该是一个类——非常有意义...多亏了你,我现在知道为什么这是关键了。
猜你喜欢
  • 2021-08-15
  • 2021-11-06
  • 2013-08-25
  • 2016-12-26
  • 2017-07-23
  • 2011-04-02
  • 2020-09-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多