You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题根源

  1. 多份数据副本:同时维护projectList和firstPersonalProject等独立变量,修改独立变量时未同步到projectList,导致scenePhase变化时被projectList的原始数据覆盖。
  2. 值类型操作问题: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),
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 09:18:29