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

问题描述

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

  • 可以从行本身编辑列表元素
  • 将过滤器逻辑移至 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",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 架构实际上只是状态和视图。在这里,它是您最感兴趣的任务的状态(完成/撤消)。使 Task 成为一个 Observable 类,用于发布它的完成/撤消状态更改。将TaskRow中的UI切换开关直接绑定到Task模型中的done/undone上(去掉中间的索引列表),那么你就不需要任何逻辑来手动发布状态变化了。

应用的第二个状态是过滤/未过滤列表。那部分你似乎已经放下了。

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

@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()
        }
    }
}
,

最后,我设法实现了具有先前列出的条件的列表功能。基于 Cenk Bilgen 答案:

  • 列表视图:
...
scenarioArray.forEach((scenario) => {
  it(`changing control value should return correct date value`,fakeAsync(() => {
    // arrange
    spyOn<any>(component,'onChange'); // Answer: spyOn private member
    component.writeValue(scenario.writtenControlValue);

    // act
    fixture.detectChanges();
    fixture.whenStable().then(() => {
      const input = fixture.debugElement.query(By.css('input')).nativeElement;
      input.value = scenario.newControlValue;
      input.dispatchEvent(new Event('input'));

      // assert
      expect(component['onChange'].calls.mostRecent().args[0].toISOString()).toEqual(
        scenario.expectedResult.toISOString()
      );
    });
  }));
});
  • 任务行:
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()
        }
    }
}
  • TaskListViewModel
struct TaskRow: View {
    
    @ObservedObject var task: TaskViewModel

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

}

请注意,当任务标记为已完成时,每个 TaskViewModel 都会将 objectWillChange 事件传播到 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)",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)

    }
}

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