问题描述
我正在尝试实现类似于 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