SwiftUI List中过滤@Published数组删除列表中的元素

Filter @Published array in SwiftUI List removes elements in list

我正在尝试实现类似于 Handling User Input 示例的列表功能,界面显示用户可以根据布尔值过滤的列表。我想添加以下与示例的不同之处:

我尝试了很多方法都没有成功,其中之一是:

    class TaskListViewModel : ObservableObject  {
    
        private var cancelables = Set<AnyCancellable>()
    
        private var allTasks: [Task] =
            [ Task(id: "1",name: "Task1", description: "Description", done: false),
              Task(id: "2",name: "Task2", description: "Description", done: false)]
    
        @Published var showNotDoneOnly = false
    
        @Published var filterdTasks: [Task] = []

        init() {
        
            filterdTasks = allTasks

            $showNotDoneOnly.map { notDoneOnly in
                if notDoneOnly {
                    return self.filterdTasks.filter { task in
                        !task.done
                    }
                }
                return self.filterdTasks
            }.assign(to: \.filterdTasks, on: self)
            .store(in: &cancelables)
        }
    }
struct TaskListView: View {
    
    @ObservedObject private var taskListViewModel = TaskListViewModel()
        
    var body: some View {
        
        NavigationView {
            VStack {
                Toggle(isOn: $taskListViewModel.showNotDoneOnly) {
                    Text("Undone only")
                }.padding()
                List {
                    ForEach(taskListViewModel.filterdTasks.indices, id: \.self) { idx in
                        TaskRow(task: $taskListViewModel.filterdTasks[idx])
                    }
                }
            }.navigationBarTitle(Text("Tasks"))
        }
    }
}
struct TaskRow: View {
    
    @Binding var task: Task

    var body: some View {
        HStack {
            Text(task.name)
            Spacer()
            Toggle("", isOn: $task.done )
        }
    }

}

使用这种方法,当用户启用过滤器时,列表会被过滤,但当它被禁用时,列表会丢失之前过滤的元素。如果我更改代码以像这样恢复过滤器元素:

    $showNotDoneOnly.map { notDoneOnly in
        if notDoneOnly {
            return self.filterdTasks.filter { task in
                !task.done
            }
        }
        return self.allTasks
    }.assign(to: \.filterdTasks, on: self)

列表丢失了编辑过的元素。

我也试过将 allTask​​ 属性 变成 @Published 字典,但没有成功。关于如何实现这个的任何想法?在 SwiftUi 中有没有更好的方法来做到这一点?

谢谢

SwiftUI 架构实际上只是状态和视图。在这里,它是您最感兴趣的任务状态 (done/undone)。使任务成为 Observable class,发布它的 done/undone 状态变化。将 TaskRow 中的 UI 切换开关直接绑定到 Task 模型中的 done/undone (删除中间索引列表),然后您不需要任何逻辑来手动发布状态更改。

应用程序的第二个状态是 filtered/unfiltered 列表。那部分你好像已经下了。

这是一种可行的方法。 编辑:这是一个关于如何保持数据状态和视图分离的更完整的示例。任务模型是这里的中心思想。

@main
struct TaskApp: App {
  @StateObject var model = Model()
  var body: some Scene {
    WindowGroup {
      TaskListView()
        .environmentObject(model)
    }
  }
}

class Model: ObservableObject {
  @Published var tasks: [Task] = [
    Task(name: "Task1", description: "Description"),
    Task(name: "Task2", description: "Description")
  ] // some initial sample data
  func updateTasks() {
    //
  }
}

class Task: ObservableObject, Identifiable, Hashable {
  var id: String { name }
  let name, description: String
  @Published var done: Bool = false
  init(name: String, description: String) {
    self.name = name
    self.description = description
  }
  static func == (lhs: Task, rhs: Task) -> Bool {
    lhs.id == rhs.id
  }
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}

struct TaskListView: View {
  @EnvironmentObject var model: Model
  var filter: ([Task]) -> [Task] = { [=10=].filter { [=10=].done } }
  @State private var applyFilter = false
  var body: some View {
    NavigationView {
      VStack {
        Toggle(isOn: $applyFilter) {
          Text("Undone only")
        }.padding()
        List {
          ForEach(
            (applyFilter ? filter(model.tasks) : model.tasks), id: \.self) { task in
            TaskRow(task: task)
          }
        }
      }.navigationBarTitle(Text("Tasks"))
    }
  }
}

struct TaskRow: View {
  @ObservedObject var task: Task
    var body: some View {
        HStack {
            Text(task.name)
            Spacer()
          Toggle("", isOn: $task.done).labelsHidden()
        }
    }
}

最后,我成功地实现了前面列出的条件下的列表功能。基于 Cenk Bilgen 答案:

  • 列表视图:
struct TaskListView: View {
    
    @ObservedObject private var viewModel = TaskListViewModel()

    var body: some View {
        
        NavigationView {
            VStack {
                Toggle(isOn: $viewModel.filterDone) {
                    Text("Filter done")
                }.padding()
                List {
                    ForEach(viewModel.filter(), id: \.self) { task in
                        TaskRow(task: task)
                    }
                }
            }.navigationBarTitle(Text("Tasks"))
        }.onAppear {
            viewModel.fetchTasks()
        }
    }
}
  • 任务行:
struct TaskRow: View {
    
    @ObservedObject var task: TaskViewModel

    var body: some View {
        HStack {
            Text(task.name)
            Spacer()
            Toggle("", isOn: $task.done )
        }
    }

}
  • 任务列表视图模型
class TaskListViewModel : ObservableObject  {
    
    private var cancelables = Set<AnyCancellable>()
    
    @Published var filterDone = false

    @Published var tasks: [TaskViewModel] = []
    
    func filter() -> [TaskViewModel]  {
        filterDone ? tasks.filter { ![=12=].done } : tasks
    }
    
    func fetchTasks() {
        let id = 0
        [
         TaskViewModel(name: "Task \(id)", description: "Description"),
         TaskViewModel(name: "Task \(id + 1)", description: "Description")
        ].forEach { add(task: [=12=]) }
    }
    
    private func add(task: TaskViewModel) {
        tasks.append(task)
        task.objectWillChange
            .sink { self.objectWillChange.send() }
            .store(in: &cancelables)

    }
}

注意这里每个 TaskViewModel 都会将 objectWillChange 事件传播到 TaskListViewModel 以在任务标记为已完成时更新过滤器。

  • 任务视图模型:
class TaskViewModel: ObservableObject, Identifiable, Hashable {
    
    var id: String { name }
    
    let name: String
    let description: String
    
    @Published var done: Bool = false
    
    init(name: String, description: String, done: Bool = false) {
        self.name = name
        self.description = description
        self.done = done
    }
    
    static func == (lhs: TaskViewModel, rhs: TaskViewModel) -> Bool {
      lhs.id == rhs.id
    }
      
    func hash(into hasher: inout Hasher) {
      hasher.combine(id)
    }
    
}

这是与原始方法的主要区别:将行模型从作为 @Binding 包含的简单结构更改为 ObservableObject