I'm learning SwiftUI and am still relatively new at it. To learn, I've been slowly building a "to-do list" style app, as it seemed relatively easy to implement for someone starting out. I probably asked the question wrong, but I'm trying to set it up so that my custom type Project has an array that stores another custom type Task, such that a project in the app can have tasks added and removed from it. I'm struggling to figure out how to remove items from the project's task list, though.
Here's the Project and Task models so far.
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")
]
}
HighestPriorityView updates in simulator when clicking the button to remove the task from the firstPersonalProject (or other categories), and the next task pops up. The problem is, when I go back to that project in another view, the task is still there... I can confirm this is because when I update the scenePhase in ContentView, all of the tasks immediately come back as if not removed from the array. Because it visually updates when clicking the button, but then resets to what was passed into the view when the scenePhase changes, I'm left to wonder if somehow I'm working with a copy of firstPersonalProject within the HighestPriorityView, versus binding to the original version of that project passed into the view? Is it even possible to remove a Task from a bound Project's task list like I'm trying to do?
Hopefully this makes sense and any advice at all is appreciated.
Here's the 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: /*@START_MENU_TOKEN@*/Text("Picker")/*@END_MENU_TOKEN@*/) {
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)
}
}
Here's MainView() that passes data into ContentView()
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: [MyTask(taskTitle: "Do the process!")])
@State var firstHomeProject: Project = Project(projectTitle: "", projectCategory: .home, taskList: [MyTask(taskTitle: "Do the process!")])
@State var firstWorkProject: Project = Project(projectTitle: "", projectCategory: .work, taskList: [MyTask(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()
}
}