SwiftUI中如何从结构体的数组中移除元素?
SwiftUI待办应用任务移除后数据重置问题解决
问题描述
构建SwiftUI待办清单应用时,自定义Project类型包含Task数组。点击按钮移除任务时界面会即时更新,但切换视图或scenePhase变化后,被移除的任务会恢复。核心原因是维护了多份数据副本,修改操作未同步到原始数据源,导致后续被覆盖。
当前代码
Project模型
import Foundation import SwiftUI struct Project: Identifiable, Codable { let id: UUID var projectTitle: String var projectNotes: String var projectCategory: Category var taskList: [Task] init(id:UUID = UUID(), projectTitle:String, projectNotes:String = "", projectCategory:Category, taskList:[Task]) { self.id = id self.projectTitle = projectTitle self.projectNotes = projectNotes self.projectCategory = projectCategory self.taskList = taskList } } extension Project { struct Data { var projectTitle:String = "" var projectNotes:String = "" var projectCategory: Category = .personal var taskList: [Task] } var data: Data { Data(projectTitle: projectTitle, projectNotes: projectNotes, projectCategory: projectCategory, taskList: taskList) } } extension Project { static let sampleData: [Project] = [ Project(projectTitle: "Mindfulness", projectCategory: .personal ,taskList: Task.sampleData), Project(projectTitle: "Clean House", projectCategory: .home, taskList: Task.sampleData), Project(projectTitle: "Finish Audit", projectCategory: .work, taskList: Task.sampleData) ] }
Task模型
import Foundation import SwiftUI struct Task: Identifiable, Hashable, Codable { let id: UUID var taskTitle: String var taskNotes: String var isTapped: Bool init(id:UUID = UUID(), taskTitle:String, taskNotes:String = "", isTapped:Bool = false) { self.id = id self.taskTitle = taskTitle self.taskNotes = taskNotes self.isTapped = isTapped } } extension Task { struct Data { var taskTitle:String = "" var taskNotes:String = "" var isTapped:Bool = false } var data: Data { Data(taskTitle: taskTitle, taskNotes: taskNotes, isTapped: isTapped) } } extension Task { static let sampleData: [Task] = [ Task(taskTitle: "Mow Lawn", taskNotes: "Make sure you set height to 4"), Task(taskTitle: "Drop off suit and blazer", taskNotes: "Has to be done this week"), Task(taskTitle: "Call Tree Trimmer", taskNotes: "Get another quote if needed") ] }
ContentView
import SwiftUI struct ContentView: View { @Binding var taskList: [Task] @Binding var firstPersonalProject: Project @Binding var firstHomeProject: Project @Binding var firstWorkProject: Project @Binding var projectList: [Project] @State private var highestPriority: Task = Task(taskTitle: "") @State private var category: Category = .personal @State private var confirmationShown = false @State private var projectToComplete: Project = Project(projectTitle: "", projectCategory: .personal, taskList: []) @Environment(\.scenePhase) private var scenePhase var body: some View { List { Section("What Should I Work On Now?") { switch category { case .personal: HighestPriorityView(completeTask: { let projectCount = $firstPersonalProject.taskList.count switch projectCount>1 { case true: firstPersonalProject.taskList.remove(at: 0) case false: completeProject(project: firstPersonalProject) } }, task: firstPersonalProject.taskList[0]) case .home: HighestPriorityView(completeTask: { let projectCount = firstHomeProject.taskList.count switch projectCount>1 { case true: firstHomeProject.taskList.remove(at: 0) case false: completeProject(project: firstHomeProject) } }, task: firstHomeProject.taskList[0]) case .work: HighestPriorityView(completeTask: { let projectCount = firstWorkProject.taskList.count switch projectCount>1 { case true: firstWorkProject.taskList.remove(at: 0) case false: completeProject(project: firstWorkProject) } }, task: firstWorkProject.taskList[0]) } } Section("Category") { Picker(selection: $category, label: Text("Picker")) { ForEach(Category.allCases) { category in Text(category.rawValue.uppercased()).tag(category) } } .pickerStyle(.segmented) } Section("Add New Tasks") { newTaskView(taskList: $taskList) } } .onChange(of: scenePhase) { newScenePhase in firstPersonalProject = projectList.first(where: {$0.projectCategory == .personal}) ?? Project(projectTitle: "No Personal Projects", projectCategory: .personal, taskList: [Task(taskTitle: "Do the process!")]) firstHomeProject = projectList.first(where: {$0.projectCategory == .home}) ?? Project(projectTitle: "No Home Projects", projectCategory: .home, taskList: [Task(taskTitle: "Do the process!")]) firstWorkProject = projectList.first(where: {$0.projectCategory == .work}) ?? Project(projectTitle: "No Work Projects", projectCategory: .work, taskList: [Task(taskTitle: "Do the process!")]) } .confirmationDialog("Complete project?", isPresented: $confirmationShown) { Button("Yes") { withAnimation { projectToComplete.taskList.remove(at: 0) if let index = projectList.firstIndex(where: {$0.projectCategory == projectToComplete.projectCategory}) { projectList.remove(at: index) } firstPersonalProject = projectList.first(where: {$0.projectCategory == .personal}) ?? Project(projectTitle: "No Personal Projects", projectCategory: .personal, taskList: [Task(taskTitle: "Do the process!")]) firstHomeProject = projectList.first(where: {$0.projectCategory == .home}) ?? Project(projectTitle: "No Home Projects", projectCategory: .home, taskList: [Task(taskTitle: "Do the process!")]) firstWorkProject = projectList.first(where: {$0.projectCategory == .work}) ?? Project(projectTitle: "No Work Projects", projectCategory: .work, taskList: [Task(taskTitle: "Do the process!")]) projectToComplete = Project(projectTitle: "", projectCategory: .personal, taskList: []) } } } message: { Text("Complete project?") } } func completeProject(project: Project) { projectToComplete = project confirmationShown = true } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(taskList: .constant(Task.sampleData), firstPersonalProject: .constant(Project.sampleData[0]), firstHomeProject: .constant(Project.sampleData[1]), firstWorkProject: .constant(Project.sampleData[2]), projectList: .constant(Project.sampleData)) } }
HighestPriorityView
import SwiftUI struct HighestPriorityView: View { var completeTask: () -> Void let task: Task var body: some View { HStack { VStack (alignment: .leading) { Text("\(task.taskTitle)".uppercased()) .font(.headline) Text("\(task.taskNotes)") .font(.caption) } Spacer() Button(action: completeTask) { Image(systemName: "star.fill") .resizable() .scaledToFit() .frame(width: 50, height: 50, alignment: .center) .foregroundColor(.yellow) } } .padding(5) } } struct HighestPriorityView_Previews: PreviewProvider { static var task: Task = Task.sampleData[0] static var previews: some View { HighestPriorityView(completeTask: {}, task: task) .previewLayout(.sizeThatFits) } }
MainView
import SwiftUI struct MainView: View { @State private var selectedView: Int = 1 @StateObject private var taskList = FileSaveLoad() @StateObject private var projectList = FileSaveLoad() @State var firstPersonalProject: Project = Project(projectTitle: "", projectCategory: .personal, taskList: [Task(taskTitle: "Do the process!")]) @State var firstHomeProject: Project = Project(projectTitle: "", projectCategory: .home, taskList: [Task(taskTitle: "Do the process!")]) @State var firstWorkProject: Project = Project(projectTitle: "", projectCategory: .work, taskList: [Task(taskTitle: "Do the process!")]) @Environment(\.scenePhase) private var scenePhase var body: some View { TabView(selection: $selectedView) { NavigationView { ContentView(taskList: $taskList.tasks, firstPersonalProject: $firstPersonalProject, firstHomeProject: $firstHomeProject, firstWorkProject: $firstWorkProject, projectList: $projectList.projects) .navigationTitle("You're Focused!") } .tabItem { Label("Focus Mode", systemImage: "star.fill") } .tag(1) NavigationView { ProcessView(taskList: $taskList.tasks, projectList: $projectList.projects, firstPersonalProject: $firstPersonalProject, firstHomeProject: $firstHomeProject, firstWorkProject: $firstWorkProject) .navigationTitle("The Process") } .tabItem { Label("Process", systemImage: "arrow.left.arrow.right") } .tag(2) } .onAppear { FileSaveLoad.loadTasks { result in switch result { case .failure(let error): fatalError(String("\(error)")) case .success(let tasks): taskList.tasks = tasks } } FileSaveLoad.loadProjects { result in switch result { case .failure(let error): fatalError(String("\(error)")) case .success(let projects): projectList.projects = projects } } } .onChange(of: scenePhase) { phase in if phase == .background { FileSaveLoad.saveTasks(tasks: taskList.tasks) { result in if case .failure(let error) = result { fatalError(String("\(error)")) } } FileSaveLoad.saveProjects(projects: projectList.projects) { result in if case .failure(let error) = result { fatalError(String("\(error)")) } } } } } } struct MainView_Previews: PreviewProvider { static var previews: some View { MainView() } }
问题根源
- 多份数据副本:同时维护
projectList和firstPersonalProject等独立变量,修改独立变量时未同步到projectList,导致scenePhase变化时被projectList的原始数据覆盖。 - 值类型操作问题:
Project是值类型,projectToComplete是副本,修改它不会影响projectList中的原始项目。
修复方案
1. 移除冗余变量
删除firstPersonalProject、firstHomeProject、firstWorkProject,直接从projectList中获取对应分类的项目,确保数据唯一来源。
2. 修改任务移除逻辑
直接操作projectList中的项目,通过id定位修改,保证修改同步到数据源。
3. 调整项目完成逻辑
直接更新projectList,避免中间变量导致的数据不同步。
修改后的关键代码
修改后的ContentView
import SwiftUI struct ContentView: View { @Binding var taskList: [Task] @Binding var projectList: [Project] @State private var category: Category = .personal @State private var confirmationShown = false @State private var projectToCompleteID: UUID? // 动态获取当前分类的项目 private var currentProject: Project? { projectList.first(where: { $0.projectCategory == category }) } var body: some View { List { Section("What Should I Work On Now?") { if let project = currentProject, !project.taskList.isEmpty { HighestPriorityView(completeTask: { guard var project = currentProject, let index = projectList.firstIndex(where: { $0.id == project.id }) else { return } if project.taskList.count > 1 { // 修改项目任务列表后,重新赋值回projectList project.taskList.remove(at: 0) projectList[index] = project } else { // 记录要完成的项目ID,避免值类型副本问题 projectToCompleteID = project.id confirmationShown = true } }, task: project.taskList[0]) } else { Text("该分类下暂无任务") } } Section("Category") { Picker(selection: $category, label: Text("选择分类")) { ForEach(Category.allCases) { category in Text(category.rawValue.uppercased()).tag(category) } } .pickerStyle(.segmented) } Section("Add New Tasks") { newTaskView(taskList: $taskList) } } .confirmationDialog("完成该项目?", isPresented: $confirmationShown) { Button("确认") { withAnimation { // 通过ID移除项目,确保操作原始数据源 if let id = projectToCompleteID { projectList.removeAll(where: { $0.id == id }) } projectToCompleteID = nil } } Button("取消", role: .cancel) {} } message: { Text("完成后该项目将被移除,确认操作?") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(taskList: .constant(Task.sampleData), projectList: .constant(Project.sampleData)) } }
修改后的MainView
import SwiftUI struct MainView: View { @State private var selectedView: Int = 1 @StateObject private var taskList = FileSaveLoad() @StateObject private var projectList = FileSaveLoad() @Environment(\.scenePhase) private var scenePhase var body: some View { TabView(selection: $selectedView) { NavigationView { // 不再传递冗余的项目变量 ContentView(taskList: $taskList.tasks, projectList: $projectList.projects) .navigationTitle("专注模式") } .tabItem { Label("Focus Mode", systemImage: "star.fill") } .tag(1) NavigationView { // ProcessView也需同步修改参数,移除冗余项目变量 ProcessView(taskList: $taskList.tasks, projectList: $projectList.projects) .navigationTitle("任务流程") } .tabItem { Label("Process", systemImage: "arrow.left.arrow.right") } .tag(2) } .onAppear { FileSaveLoad.loadTasks { result in switch result { case .failure(let error): fatalError(String("\(error)")) case .success(let tasks): taskList.tasks = tasks } } FileSaveLoad.loadProjects { result in switch result { case .failure(let error): fatalError(String("\(error)")) case .success(let projects): projectList.projects = projects } } } .onChange(of: scenePhase) { phase in if phase == .background { FileSaveLoad.saveTasks(tasks: taskList.tasks) { result in if case .failure(let error) = result { fatalError(String("\(error)")) } } FileSaveLoad.saveProjects(projects: projectList.projects) { result in if case .failure(let error) = result { fatalError(String("\(error)")) } } } } } } struct MainView_Previews: PreviewProvider { static var previews: some View { MainView() } }
额外建议
- 避免维护多份相同数据源,始终以
projectList作为唯一数据来源,减少同步问题。 - 对于值类型(如
Project、Task),
相关产品推荐
相关产品推荐

