简体   繁体   English

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

[英]Filter @Published array in SwiftUI List removes elements in list

I am trying to implement a list functionality similar to to Handling User Input example, the interface shows a list the user can filter depending on boolean values.我正在尝试实现类似于处理用户输入示例的列表功能,界面显示用户可以根据 boolean 值过滤的列表。 I want to add the following differences from the example:我想从示例中添加以下差异:

  • Can edit list elements from the row itself可以从行本身编辑列表元素
  • Move the filter logic to a ViewModel class将过滤器逻辑移动到 ViewModel class

I've tried many approaches without success one of them:我尝试了许多方法但没有成功,其中一种:

  • ViewModel:视图模型:
    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)
        }
    }
  • View:看法:
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"))
        }
    }
}
  • TaskRow:任务行:
struct TaskRow: View {
    
    @Binding var task: Task

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

}

With this approach the list is filtered when the user enable the filter but when it is disabled the list lose the previously filtered elements.使用这种方法,当用户启用过滤器时,列表会被过滤,但当它被禁用时,列表会丢失先前过滤的元素。 If I change the code to restore the filter elements like this:如果我更改代码以恢复过滤器元素,如下所示:

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

The list lose the edited elements.该列表会丢失已编辑的元素。

I've also tried making allTask property to a @Published dictionary by without success.我也尝试过将 allTask 属性设置为 @Published 字典,但没有成功。 Any idea on how to implement this?关于如何实现这一点的任何想法? Is ther any better approach to do this in SwiftUi?在 SwiftUi 中有没有更好的方法来做到这一点?

Thanks谢谢

SwiftUI architecture is really just state and view. SwiftUI 架构实际上只是 state 和视图。 Here, it's the state of the Task that you are most interested in (done/undone).在这里,它是您最感兴趣的任务的 state(完成/撤消)。 Make the Task an Observable class that publishes it's done/undone state change.使任务成为可观察的 class 发布它的完成/撤消 state 更改。 Bind the UI toggle switch in TaskRow directly to that done/undone in the Task model (remove the intermediary list of indexes), then you don't need any logic to publish state changes manually.将 TaskRow 中的 UI 切换开关直接绑定到 Task model 中的完成/撤消(删除中间索引列表),然后您不需要任何逻辑来手动发布 state 更改。

The second state for the app is filtered/unfiltered for the list.应用程序的第二个 state 针对列表进行了过滤/未过滤。 That part it seems you already have down.那部分似乎你已经放下了。

This is one possible way to do it.这是一种可能的方法。 EDIT: Here's a more full example on how to keep the data state and view separate.编辑:这是一个更完整的示例,说明如何保留数据 state 并单独查看。 The Task model is the central idea here.任务 model 是这里的中心思想。

@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] = { $0.filter { $0.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()
        }
    }
}

Finally I've managed to implement the list functionality whith the conditions previously listed.最后,我设法实现了前面列出的条件的列表功能。 Based on Cenk Bilgen answer:基于Cenk Bilgen的回答:

  • ListView:列表显示:
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()
        }
    }
}
  • TaskRow:任务行:
struct TaskRow: View {
    
    @ObservedObject var task: TaskViewModel

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

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

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

    }
}

Notice here each TaskViewModel will propagate objectWillChange event to TaskListViewModel to update the filter when a task is marked as completed.请注意,每个 TaskViewModel 都会将 objectWillChange 事件传播到 TaskListViewModel 以在任务标记为已完成时更新过滤器。

  • TaskViewModel:任务视图模型:
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)
    }
    
}

This is the main difference from the original approach: Changing the row model from a simple struct included as @Binding to an ObservableObject这是与原始方法的主要区别:将行 model 从包含为 @Binding 的简单结构更改为 ObservableObject

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM