SwiftUI List中过滤@Published数组删除列表中的元素
Filter @Published array in SwiftUI List removes elements in list
我正在尝试实现类似于 Handling User Input 示例的列表功能,界面显示用户可以根据布尔值过滤的列表。我想添加以下与示例的不同之处:
- 可以从行本身编辑列表元素
- 将过滤器逻辑移动到 ViewModel class
我尝试了很多方法都没有成功,其中之一是:
- 视图模型:
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
我正在尝试实现类似于 Handling User Input 示例的列表功能,界面显示用户可以根据布尔值过滤的列表。我想添加以下与示例的不同之处:
- 可以从行本身编辑列表元素
- 将过滤器逻辑移动到 ViewModel class
我尝试了很多方法都没有成功,其中之一是:
- 视图模型:
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