diff --git a/process.xcodeproj/project.xcworkspace/xcuserdata/maxfierro.xcuserdatad/UserInterfaceState.xcuserstate b/process.xcodeproj/project.xcworkspace/xcuserdata/maxfierro.xcuserdatad/UserInterfaceState.xcuserstate index 1a0e5ef..ac9913b 100644 Binary files a/process.xcodeproj/project.xcworkspace/xcuserdata/maxfierro.xcuserdatad/UserInterfaceState.xcuserstate and b/process.xcodeproj/project.xcworkspace/xcuserdata/maxfierro.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/process/Components/ProjectPicker.swift b/process/Components/ProjectPicker.swift index 546f2c6..138dbcf 100644 --- a/process/Components/ProjectPicker.swift +++ b/process/Components/ProjectPicker.swift @@ -26,16 +26,11 @@ struct ProjectsListItemView: View { class ProjectPickerViewModel: ObservableObject { var parentModel: TaskMeddlerModel - @Published var projectID: String - @Published var project: Project = Project(creatorID: "") + @Published var project: Project - init(projectID: String, parentModel: TaskMeddlerModel) { - self.projectID = projectID - self.parentModel = parentModel - Project.pull(projectID) { project, error in - guard error == nil else { return } - self.project = project! - } + init(project: Project, model: TaskMeddlerModel) { + self.parentModel = model + self.project = project } func setToProject() { diff --git a/process/Components/SearchBar.swift b/process/Components/SearchBar.swift index d98bdc2..05009dd 100644 --- a/process/Components/SearchBar.swift +++ b/process/Components/SearchBar.swift @@ -39,12 +39,16 @@ struct SearchBar: View { } ) .onTapGesture { - isEditingSearch = true + withAnimation { + isEditingSearch = true + } } if isEditingSearch { Button { - isEditingSearch = false - searchText = "" + withAnimation { + isEditingSearch = false + searchText = "" + } } label: { Text("Cancel") } diff --git a/process/Components/TaskList.swift b/process/Components/TaskList.swift index 46855a8..820db8a 100644 --- a/process/Components/TaskList.swift +++ b/process/Components/TaskList.swift @@ -11,17 +11,17 @@ import SwiftUI struct TaskListView: View { - @ObservedObject var model: TaskListViewModel + @StateObject var model: TaskListViewModel /* MARK: Task List */ var body: some View { ScrollView(.vertical) { LazyVGrid(columns: [GridItem()], spacing: 8) { - ForEach(model.taskList.items) { item in - TaskCellView(model: TaskCellViewModel(item.task)) + ForEach(model.taskList.tasks) { item in + TaskCellView(model: TaskCellViewModel(item)) .onTapGesture { - model.tappedTask(item.task) + model.tappedTask(item) } } } @@ -29,7 +29,6 @@ struct TaskListView: View { } } - class TaskListViewModel: ObservableObject { @Published var taskList: AsyncTaskList @@ -63,15 +62,22 @@ struct TaskCellView: View { } .padding(.top, 1) HStack { - Text(model.formattedDueDate()) + Text(model.formattedDate(string: "Due: ", date: model.task.data.dateDue)) .font(.caption2) Spacer() - Text(String(model.task.data.size)) + Text("Size: " + String(model.task.data.size)) .font(.caption2) } .padding(.top, 8) } label: { - Text(model.task.data.name) + HStack { + Text(model.task.data.name) + if model.task.data.dateCompleted != nil { + Spacer() + Text(model.formattedDate(string: "Completed: ", date: model.task.data.dateCompleted!)) + .font(.caption2) + } + } } } } @@ -94,11 +100,11 @@ class TaskCellViewModel: ObservableObject { return description } - func formattedDueDate() -> String { + func formattedDate(string: String, date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MMM d, yyyy" dateFormatter.timeZone = NSTimeZone(name: "PST")! as TimeZone - return "Due " + dateFormatter.string(from: task.data.dateDue) + return string + dateFormatter.string(from: date) } } diff --git a/process/Features/Home/ProfileHome.swift b/process/Features/Home/ProfileHome.swift index 1b43836..8b15b64 100644 --- a/process/Features/Home/ProfileHome.swift +++ b/process/Features/Home/ProfileHome.swift @@ -30,21 +30,21 @@ struct ProfileHomeView: View { model.tappedEditProfile() } } - if (!model.editing) { - AnalyticsScrollView(model: model) - } +// if (!model.editing) { +// AnalyticsScrollView(model: model) +// } Spacer() } .accentColor(GlobalConstant.accentColor) .banner(data: $model.bannerData, show: $model.showBanner) .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - model.tappedPreferences() - } label: { - Label(ProfileConstant.preferencesAccessibilityText, systemImage: ProfileConstant.preferencesButtonIcon) - } - } +// ToolbarItemGroup(placement: .navigationBarTrailing) { +// Button { +// model.tappedPreferences() +// } label: { +// Label(ProfileConstant.preferencesAccessibilityText, systemImage: ProfileConstant.preferencesButtonIcon) +// } +// } ToolbarItemGroup(placement: .navigationBarLeading) { Button { withAnimation { diff --git a/process/Features/Home/ProjectsHome.swift b/process/Features/Home/ProjectsHome.swift index b0ed491..29c7857 100644 --- a/process/Features/Home/ProjectsHome.swift +++ b/process/Features/Home/ProjectsHome.swift @@ -29,17 +29,20 @@ struct ProjectsHomeView: View { .padding(.horizontal) .padding(.vertical, 8) - SegmentedPicker(accessibilityText: ProjectsConstant.pickerAccessibilityText, - categories: model.projectCategories, - selectedCategory: $model.selectedProjectCategory) - .padding(.horizontal) - .padding(.bottom) +// SegmentedPicker(accessibilityText: ProjectsConstant.pickerAccessibilityText, +// categories: model.projectCategories, +// selectedCategory: $model.selectedProjectCategory) +// .padding(.horizontal) +// .padding(.bottom) ScrollView { LazyVGrid(columns: model.twoColumnGrid, spacing: 8) { - ForEach($model.user.data.allProjects.indices, id: \.self) { index in - ProjectCellView(model: ProjectCellViewModel(projectID: model.user.data.allProjects[index], - model: model)) + ForEach(model.user.projectList) { project in + ProjectCellView(model: ProjectCellViewModel(project: project)) + .onTapGesture { + model.selectedProject = project + model.navigateToProjectDetails = true + } } } } @@ -53,13 +56,13 @@ struct ProjectsHomeView: View { .banner(data: $model.bannerData, show: $model.showBanner) .accentColor(GlobalConstant.accentColor) .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - model.tappedNotifications() - } label: { - Label(ProjectsConstant.notificationsAccessibilityText, systemImage: ProjectsConstant.notificationsButtonIcon) - } - } +// ToolbarItemGroup(placement: .navigationBarTrailing) { +// Button { +// model.tappedNotifications() +// } label: { +// Label(ProjectsConstant.notificationsAccessibilityText, systemImage: ProjectsConstant.notificationsButtonIcon) +// } +// } ToolbarItemGroup(placement: .navigationBarLeading) { Button { withAnimation { diff --git a/process/Features/Home/TasksHome.swift b/process/Features/Home/TasksHome.swift index 269be58..c10311d 100644 --- a/process/Features/Home/TasksHome.swift +++ b/process/Features/Home/TasksHome.swift @@ -14,14 +14,18 @@ struct TasksHomeView: View { @ObservedObject var model: TasksHomeViewModel @Environment(\.colorScheme) private var colorScheme - + /* MARK: Tasks home view */ var body: some View { VStack { - NavigationLink(destination: ExportTasksView(), tag: true, selection: $model.navigateToExport) { } + NavigationLink(destination: ExportTasksView(), + tag: true, + selection: $model.navigateToExport) { } - NavigationLink(destination: TaskDetailsView(model: TaskDetailsViewModel(model)), tag: true, selection: $model.navigateToTaskDetails) { } + NavigationLink(destination: TaskDetailsView(model: TaskDetailsViewModel(model)), + tag: true, + selection: $model.navigateToTaskDetails) { } SearchBar(searchText: $model.searchText, isEditingSearch: $model.isEditingSearch, sortSelection: $model.sortSelection) .padding(.horizontal) @@ -30,11 +34,11 @@ struct TasksHomeView: View { model.changedTaskSort(sortType: newSortSelection) } - SegmentedPicker(accessibilityText: TasksConstant.pickerAccessibilityText, - categories: model.taskCategories, - selectedCategory: $model.selectedTaskCategory) - .padding(.horizontal) - .padding(.bottom) +// SegmentedPicker(accessibilityText: TasksConstant.pickerAccessibilityText, +// categories: model.taskCategories, +// selectedCategory: $model.selectedTaskCategory) +// .padding(.horizontal) +// .padding(.bottom) TaskListView(model: TaskListViewModel(model)) .padding(.horizontal) @@ -45,16 +49,15 @@ struct TasksHomeView: View { model.tappedNewTask() } .accentColor(GlobalConstant.accentColor) - .onAppear(perform: model.refreshTaskList) .banner(data: $model.bannerData, show: $model.showBanner) .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - model.tappedExport() - } label: { - Label(TasksConstant.exportAccessibilityText, systemImage: TasksConstant.exportButtonIcon) - } - } +// ToolbarItemGroup(placement: .navigationBarTrailing) { +// Button { +// model.tappedExport() +// } label: { +// Label(TasksConstant.exportAccessibilityText, systemImage: TasksConstant.exportButtonIcon) +// } +// } ToolbarItemGroup(placement: .navigationBarLeading) { Button { withAnimation { @@ -78,6 +81,10 @@ struct TasksHomeView: View { obtain data and to communicate instructions, such as logging out. */ class TasksHomeViewModel: TaskListParent, ObservableObject { + func refreshTaskList() { + return + } + /* MARK: Model fields */ var parentModel: HomeViewModel @@ -112,7 +119,7 @@ class TasksHomeViewModel: TaskListParent, ObservableObject { init(_ parentModel: HomeViewModel) { self.user = parentModel.user - self.taskList = AsyncTaskList(parentModel.user.data.tasks) + self.taskList = parentModel.user.taskList self.parentModel = parentModel } @@ -136,10 +143,11 @@ class TasksHomeViewModel: TaskListParent, ObservableObject { func changedTaskSort(sortType: TaskSort) { if sortType == .topological { - self.taskList = self.taskList.getTopologicalOrdering() + self.taskList.getTopologicalOrdering() } else { self.taskList.sort(sortType) } + self.objectWillChange.send() } func dismissChildView(_ named: String) { @@ -148,6 +156,8 @@ class TasksHomeViewModel: TaskListParent, ObservableObject { self.navigateToNewTask = false case "ExportTasksView": self.navigateToExport = false + case "TaskDetailsView": + self.navigateToTaskDetails = false default: return } @@ -155,9 +165,9 @@ class TasksHomeViewModel: TaskListParent, ObservableObject { /* MARK: Helper methods */ - func refreshTaskList() { - self.taskList = AsyncTaskList(parentModel.user.data.tasks) - } +// func refreshTaskList() { +// self.taskList = AsyncTaskList(parentModel.user.data.tasks) +// } func showBannerWithErrorMessage(_ message: String?) { guard let message = message else { return } diff --git a/process/Features/Home/Views/ProjectCell.swift b/process/Features/Home/Views/ProjectCell.swift index 5ff907b..29860be 100644 --- a/process/Features/Home/Views/ProjectCell.swift +++ b/process/Features/Home/Views/ProjectCell.swift @@ -35,7 +35,9 @@ struct ProjectCellView: View { Spacer() } HStack { - ProgressView(model.formattedCreationDate(), value: 50, total: 100) + ProgressView(model.formattedCreationDate(), + value: Float(model.completedTaskCount()), + total: Float(model.project.taskList.allTasks.count)) .progressViewStyle(.linear) .font(.caption2) } @@ -45,39 +47,26 @@ struct ProjectCellView: View { } label: { Text(model.project.data.name) } - .onTapGesture { - model.tappedProject() - } } } class ProjectCellViewModel: ObservableObject { - @Published var projectsHomeViewModel: ProjectsHomeViewModel @Published var project: Project = Project(creatorID: "") - @Published var collaboratorPictures: [UIImage] = [] - init(projectID: String, model: ProjectsHomeViewModel) { - self.projectsHomeViewModel = model - Project.pull(projectID) { project, error in - guard error == nil else { return } - self.project = project! - for index in project!.data.collaborators.indices { - self.collaboratorPictures.append(UIImage(named: ProfileConstant.defaultProfilePicture)!) - APIHandler.pullProfilePicture(userID: project!.data.collaborators[index]) { error, image in - guard error == nil else { return } - self.collaboratorPictures[index] = image! - } + init(project: Project) { + self.project = project + for index in project.data.collaborators.indices { + self.collaboratorPictures.append(UIImage(named: ProfileConstant.defaultProfilePicture)!) + APIHandler.pullProfilePicture(userID: project.data.collaborators[index]) { error, image in + guard error == nil else { return } + self.collaboratorPictures[index] = image! } } } - func tappedProject() { - self.projectsHomeViewModel.showProjectDetails(project: self.project) - } - func formattedDescription() -> String { var description: String = project.data.description ?? "" if description.count > 30 { @@ -93,6 +82,15 @@ class ProjectCellViewModel: ObservableObject { return "Started " + dateFormatter.string(from: project.data.dateCreated) } + func completedTaskCount() -> Int { + var count: Int = 0 + for task in project.taskList.allTasks { + if task.data.dateCompleted != nil { + count += 1 + } + } + return count + } } diff --git a/process/Features/Projects/EditProject.swift b/process/Features/Projects/EditProject.swift index 06dc392..2193154 100644 --- a/process/Features/Projects/EditProject.swift +++ b/process/Features/Projects/EditProject.swift @@ -18,3 +18,4 @@ struct EditProjectView_Previews: PreviewProvider { EditProjectView() } } + diff --git a/process/Features/Projects/NewProject.swift b/process/Features/Projects/NewProject.swift index 4a636aa..6027ff9 100644 --- a/process/Features/Projects/NewProject.swift +++ b/process/Features/Projects/NewProject.swift @@ -93,13 +93,13 @@ class NewProjectViewModel: ObservableObject { let newProject = Project(creatorID: self.user.data.id) self.user - .addOwnedProject(newProject.data.id) + .addOwnedProject(newProject) .push { error in guard error == nil else { self.showBannerWithErrorMessage(error?.localizedDescription) return } - + newProject .changeName(self.titleField) .changeOwner(self.user.data.id) diff --git a/process/Features/Projects/ProjectDetails.swift b/process/Features/Projects/ProjectDetails.swift index c71f652..787d663 100644 --- a/process/Features/Projects/ProjectDetails.swift +++ b/process/Features/Projects/ProjectDetails.swift @@ -49,18 +49,18 @@ struct ProjectDetailsView: View { .onAppear(perform: model.refreshTaskList) .banner(data: $model.bannerData, show: $model.showBanner) .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - model.tappedAddCollaborator() - } label: { - Label("Add collaborator", systemImage: "person.fill.badge.plus") - } - Button { - model.tappedEditProject() - } label: { - Label("Edit project", systemImage: "square.and.pencil") - } - } +// ToolbarItemGroup(placement: .navigationBarTrailing) { +// Button { +// model.tappedAddCollaborator() +// } label: { +// Label("Add collaborator", systemImage: "person.fill.badge.plus") +// } +// Button { +// model.tappedEditProject() +// } label: { +// Label("Edit project", systemImage: "square.and.pencil") +// } +// } } .sheet(isPresented: $model.navigateToNewTask) { NavigationView { @@ -111,7 +111,7 @@ class ProjectDetailsViewModel: TaskListParent, ObservableObject { self.user = model.user self.parentViewModel = model self.project = model.selectedProject - self.taskList = AsyncTaskList(model.selectedProject.data.tasks) + self.taskList = model.selectedProject.taskList } /* MARK: Model action methods */ @@ -148,7 +148,7 @@ class ProjectDetailsViewModel: TaskListParent, ObservableObject { /* MARK: Model helper methods */ func refreshTaskList() { - self.taskList = AsyncTaskList(self.project.data.tasks) // FIXME: Might not update properly +// self.taskList = AsyncTaskList(self.project.data.tasks) // FIXME: Might not update properly } func showBannerWithSuccessMessage(_ message: String?) { diff --git a/process/Features/Tasks/EditTask.swift b/process/Features/Tasks/EditTask.swift index 1852594..110e08a 100644 --- a/process/Features/Tasks/EditTask.swift +++ b/process/Features/Tasks/EditTask.swift @@ -91,8 +91,8 @@ struct EditTaskView: View { GroupBox { ScrollView(.horizontal) { HStack { - ForEach($model.user.data.allProjects.indices, id: \.self) { index in - ProjectsListItemView(model: ProjectPickerViewModel(projectID: model.user.data.allProjects[index], parentModel: model)) + ForEach(model.user.projectList) { project in + ProjectsListItemView(model: ProjectPickerViewModel(project: project, model: model)) } } } @@ -196,30 +196,22 @@ class EditTaskViewModel: TaskMeddlerModel, ObservableObject { } func tappedSave() { - let editingTask = self.task + var fromProject: String? = self.task.data.project + + self.task + .changeName(self.titleField) + .changeSize(self.size) + .changeDescription(self.descriptionField) + .changeDateDue(self.dateDue) + .changeAssignee(self.user.data.id) + .changeProject(self.toProject) + .finishEdit() self.user - .addTask(editingTask.data.id) - .push { error in - guard error == nil else { - self.showBannerWithErrorMessage(error?.localizedDescription) - return - } - editingTask - .changeName(self.titleField) - .changeSize(self.size) - .changeDescription(self.descriptionField) - .changeDateDue(self.dateDue) - .changeAssignee(self.user.data.id) - .changeProject(self.toProject) - .push { error in - guard error == nil else { - self.showBannerWithErrorMessage(error?.localizedDescription) - return - } - self.dismissView(successBanner: "We have created and saved your new task!") - } - } + .addTaskToMyProject(self.task, fromProject) + .finishEdit() + + self.dismissView(successBanner: "We have saved your task!") } func getDueDateSuggestion() { @@ -250,22 +242,30 @@ class EditTaskViewModel: TaskMeddlerModel, ObservableObject { self.showBannerWithErrorMessage(error?.localizedDescription) return } - self.parentModel.showBannerWithSuccessMessage("We have erased your task from existence.") self.parentModel.dismissChildView("EditTaskView") + self.parentModel.parentModel.dismissChildView("TaskDetailsView") + self.parentModel.parentModel.showBannerWithSuccessMessage("We have erased your task from existence.") } } } func tappedComplete() { - self.task + let task1 = self.task + task1 .complete() + .finishEdit() + self.user + .removeTask(self.task.data.id) + .addTask(task1) .push { error in - guard error == nil else { - self.showBannerWithErrorMessage(error?.localizedDescription) - return - } - self.parentModel.dismissChildView("EditTaskView") + guard error == nil else { + self.showBannerWithErrorMessage(error?.localizedDescription) + return } + self.parentModel.dismissChildView("EditTaskView") + self.parentModel.parentModel.dismissChildView("TaskDetailsView") + self.parentModel.parentModel.showBannerWithSuccessMessage("We have erased your task from existence.") + } } /* MARK: Helper methods */ diff --git a/process/Features/Tasks/NewTask.swift b/process/Features/Tasks/NewTask.swift index d8dfbce..d06b64f 100644 --- a/process/Features/Tasks/NewTask.swift +++ b/process/Features/Tasks/NewTask.swift @@ -79,8 +79,8 @@ struct NewTaskView: View { GroupBox { ScrollView(.horizontal) { HStack { - ForEach($model.user.data.allProjects.indices, id: \.self) { index in - ProjectsListItemView(model: ProjectPickerViewModel(projectID: model.user.data.allProjects[index], parentModel: model)) + ForEach(model.user.projectList) { project in + ProjectsListItemView(model: ProjectPickerViewModel(project: project, model: model)) } } } @@ -173,29 +173,22 @@ class NewTaskViewModel: TaskMeddlerModel, ObservableObject { func tappedSave() { let newTask = Task(creatorID: self.user.data.id) - self.user - .addTask(newTask.data.id) - .push { error in - guard error == nil else { - self.showBannerWithErrorMessage(error?.localizedDescription) - return - } - newTask - .changeName(self.titleField) - .changeSize(self.size) - .changeDescription(self.descriptionField) - .changeDateDue(self.dateDue) - .changeAssignee(self.user.data.id) - .changeProject(self.toProject) - .push { error in - guard error == nil else { - self.showBannerWithErrorMessage(error?.localizedDescription) - return - } - self.parentModel.refreshTaskList() - self.dismissView(successBanner: "We have created and saved your new task!") - } - } + + newTask + .changeName(self.titleField) + .changeSize(self.size) + .changeDescription(self.descriptionField) + .changeDateDue(self.dateDue) + .changeAssignee(self.user.data.id) + .changeProject(self.toProject) + .finishEdit() + + self.user + .addTask(newTask) + .addTaskToMyProject(newTask, nil) + .finishEdit() + + self.dismissView(successBanner: "We have created and saved your new task!") } /* MARK: Helper methods */ diff --git a/process/Features/Tasks/SelectSubtasks.swift b/process/Features/Tasks/SelectSubtasks.swift index 5cb42d7..4905a15 100644 --- a/process/Features/Tasks/SelectSubtasks.swift +++ b/process/Features/Tasks/SelectSubtasks.swift @@ -61,11 +61,8 @@ class SelectSubtasksViewModel: TaskListParent, ObservableObject { func tappedTask() { self.thisTask - .addSubtask(selectedTask.data.id) - .push() { error in - guard error == nil else { return } - self.dismissView(successBanner: nil) - } + .addSubtask(self.selectedTask) + self.dismissView(successBanner: nil) } private func dismissView(successBanner: String?) { diff --git a/process/Features/Tasks/TaskDetails.swift b/process/Features/Tasks/TaskDetails.swift index 79088a8..6811b2c 100644 --- a/process/Features/Tasks/TaskDetails.swift +++ b/process/Features/Tasks/TaskDetails.swift @@ -112,7 +112,7 @@ class TaskDetailsViewModel: TaskListParent, ObservableObject { self.user = model.user self.parentModel = model self.thisTask = model.selectedTask - self.taskList = AsyncTaskList(model.selectedTask.data.subtasks) + self.taskList = model.selectedTask.subtaskList } /* MARK: Action methods */ @@ -138,10 +138,12 @@ class TaskDetailsViewModel: TaskListParent, ObservableObject { default: return } + self.refreshTaskList() + self.objectWillChange.send() } func refreshTaskList() { - self.taskList = AsyncTaskList(self.thisTask.data.subtasks) + self.taskList = AsyncTaskList(self.thisTask.subtaskList.tasks) } func showBannerWithSuccessMessage(_ message: String?) { diff --git a/process/Models/AsyncTaskList.swift b/process/Models/AsyncTaskList.swift index 06c7ba1..cf8672e 100644 --- a/process/Models/AsyncTaskList.swift +++ b/process/Models/AsyncTaskList.swift @@ -54,46 +54,84 @@ class AsyncTaskList: ObservableObject { /* MARK: Fields */ - @Published var items: [TaskListItem] = [] +// @Published var items: [TaskListItem] = [] + + @Published var allTasks: [Task] = [] + @Published var tasks: [Task] = [] + @Published var taskDict: [String: Task] = [:] // Adjacency dictionary mapping taskIDs to a list of subtask IDs var digraph: [String : [String]] = [:] /* MARK: Methods */ - init(_ taskIDList: [String]) { - for taskID in taskIDList { - self.items.append(TaskListItem(taskID)) +// init(_ taskIDList: [String]) { +// for taskID in taskIDList { +// self.items.append(TaskListItem(taskID)) +// } +// } + + init(_ taskList: [Task]) { + for task in taskList { + self.insertTask(task) } } - func insertTask(_ taskID: String) { - items.append(TaskListItem(taskID)) +// func insertTask(_ taskID: String) { +// items.append(TaskListItem(taskID)) +// } + + func insertTask(_ task: Task) { + tasks.append(task) + allTasks.append(task) + self.taskDict[task.data.id] = task + } + + func removeTask(_ taskID: String) { + self.taskDict.removeValue(forKey: taskID) + self.tasks.removeAll { task in + return task.data.id == taskID + } + self.allTasks.removeAll { task in + return task.data.id == taskID + } } func sort(_ sort: TaskSort) { switch sort { case .recentlyCreated: - self.items.sort { i, j in - return i.task.data.dateCreated > j.task.data.dateCreated // FIXME: Compare dates as strings + self.tasks.sort { i, j in + return i.data.dateCreated > j.data.dateCreated // FIXME: Compare dates as strings } case .soonestDue: - self.items.sort { i, j in - return i.task.data.dateDue > j.task.data.dateDue // FIXME: Compare dates as strings + self.tasks.sort { i, j in + return i.data.dateDue > j.data.dateDue // FIXME: Compare dates as strings } case .smallest: - self.items.sort { i, j in - return i.task.data.size > j.task.data.size // FIXME: Might be the wrong way around + self.tasks.sort { i, j in + return i.data.size > j.data.size // FIXME: Might be the wrong way around } case .largest: - self.items.sort { i, j in - return i.task.data.size < j.task.data.size // FIXME: Might be the wrong way around + self.tasks.sort { i, j in + return i.data.size < j.data.size // FIXME: Might be the wrong way around } default: return } } + func getDoneTasks() { + self.tasks = self.allTasks.filter { task in + return task.data.dateCompleted != nil + } + } + + func getUnfinishedTasks() { + self.tasks = self.allTasks.filter { task in + return task.data.dateCompleted == nil + } + } + /* MARK: Task graph algorithms */ /** Non-destructively returns another instance of AsyncTaskList containing @@ -112,14 +150,17 @@ class AsyncTaskList: ObservableObject { // Set of all tasks, and the set of task not available as subtasks var taskSet = Set(graph.keys) var upstreamTaskSet: Set = [] + let directSubtasks: Set = Set(self.digraph[task.data.id]!) // Tasks which are 'upstream' of TASK, which cannot be its subtasks upstreamTaskSet = Set(self.preorderTraversal(dict: transpose, fromNode: task.data.id)) - // The relative complement of all tasks with those 'upstream' + // The relative complement of all tasks with those 'upstream' and those + // which are already subtasks taskSet.subtract(upstreamTaskSet) + taskSet.subtract(directSubtasks) - return AsyncTaskList(Array(taskSet)) + return AsyncTaskList(getTaskFromIDs(Array(taskSet))) } @@ -130,7 +171,7 @@ class AsyncTaskList: ObservableObject { Where V and E are the count of vertices and edges, the time complexity is in O(V + E). */ - func getTopologicalOrdering() -> AsyncTaskList { + func getTopologicalOrdering() { // Preprocessing, ensure changes since last sort are considered self.initializeGraph() @@ -155,9 +196,9 @@ class AsyncTaskList: ObservableObject { // Verify there are no edges left, implying no cycles if topologicalOrdering.count == self.digraph.keys.count { - return AsyncTaskList(topologicalOrdering) + self.tasks = getTaskFromIDs(topologicalOrdering.reversed()) } else { - return self + return } } @@ -180,11 +221,11 @@ class AsyncTaskList: ObservableObject { of). */ private func getDigraphIndigrees() -> [String: Int] { var indegree: [String: Int] = [:] - for task in self.items { - indegree[task.getID()] = 0 + for task in self.tasks { + indegree[task.data.id] = 0 } - for task in self.items { - for subtask in task.getSubtasks() { + for task in self.tasks { + for subtask in task.data.subtasks { indegree[subtask]! += 1 } } @@ -220,9 +261,9 @@ class AsyncTaskList: ObservableObject { // Quadratic, but only runs once private func populateTaskGraph() { - for item in self.items { - for subtask in item.getSubtasks() { - self.addEdge(from: item.getID(), to: subtask) + for task in self.tasks { + for subtask in task.data.subtasks { + self.addEdge(from: task.data.id, to: subtask) } } } @@ -230,8 +271,8 @@ class AsyncTaskList: ObservableObject { // Linear, only runs once private func initializeGraph() { self.digraph = [:] - for item in self.items { - digraph[item.getID()] = [] + for task in self.tasks { + digraph[task.data.id] = [] } } @@ -239,36 +280,44 @@ class AsyncTaskList: ObservableObject { private func addEdge(from: String, to: String) { digraph[from]?.append(to) } + + private func getTaskFromIDs(_ ids: [String]) -> [Task] { + var result: [Task] = [] + for item in ids { + result.append(taskDict[item]!) + } + return result + } } /** Helper class for the task collection, which facilitates downloading many tasks into a single TaskCollections in one go. */ -class TaskListItem: ObservableObject, Hashable, Identifiable { - - @Published var task: Task = Task(creatorID: "") - - init(_ taskID: String) { - Task.pull(taskID) { task, error in - guard error == nil else { return } - self.task = task! - } - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } - - public static func ==(lhs: TaskListItem, rhs: TaskListItem) -> Bool { - return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) - } - - func getID() -> String { - return self.task.data.id - } - - func getSubtasks() -> [String] { - return self.task.data.subtasks - } - -} +//class TaskListItem: ObservableObject, Hashable, Identifiable { +// +// @Published var task: Task = Task(creatorID: "") +// +// init(_ taskID: String) { +// Task.pull(taskID) { task, error in +// guard error == nil else { return } +// self.task = task! +// } +// } +// +// public func hash(into hasher: inout Hasher) { +// hasher.combine(ObjectIdentifier(self)) +// } +// +// public static func ==(lhs: TaskListItem, rhs: TaskListItem) -> Bool { +// return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) +// } +// +// func getID() -> String { +// return self.task.data.id +// } +// +// func getSubtasks() -> [String] { +// return self.task.data.subtasks +// } +// +//} diff --git a/process/Models/Project.swift b/process/Models/Project.swift index 6ec628e..7d7e20c 100644 --- a/process/Models/Project.swift +++ b/process/Models/Project.swift @@ -11,7 +11,7 @@ import Foundation /** Intermediary class for performing operations on projects. Can be seen as a singleton class for every relevant project. */ -class Project: Hashable { +class Project: Hashable, Identifiable { var data: ProjectData var taskList: AsyncTaskList @@ -20,7 +20,7 @@ class Project: Hashable { init(_ data: ProjectData) { self.data = data - self.taskList = AsyncTaskList(data.tasks) + self.taskList = AsyncTaskList([]) } init(creatorID: String) { @@ -40,10 +40,10 @@ class Project: Hashable { func finishEdit() { return } - func refreshTaskList() -> Project { - self.taskList = AsyncTaskList(self.data.tasks) - return self - } +// func refreshTaskList() -> Project { +// self.taskList = AsyncTaskList(self.data.tasks) +// return self +// } func changeName(_ name: String) -> Project { self.data = ProjectData(copyOf: self.data, @@ -102,15 +102,17 @@ class Project: Hashable { return self } - func addTask(_ taskID: String) -> Project { - if !self.data.tasks.contains(taskID) { - self.data.tasks.append(taskID) + func addTask(_ task: Task) -> Project { + if !self.data.tasks.contains(task.data.id) { + self.data.tasks.append(task.data.id) + self.taskList.insertTask(task) } return self } func removeTask(_ taskID: String) -> Project { self.data.tasks.removeAll { $0 == taskID } + self.taskList.allTasks.removeAll { $0.data.id == taskID } return self } diff --git a/process/Models/Task.swift b/process/Models/Task.swift index 7f64c27..258ef5c 100644 --- a/process/Models/Task.swift +++ b/process/Models/Task.swift @@ -13,16 +13,15 @@ enum TaskSize: Int { case big = 3 } -class Task: Hashable { +class Task: Hashable, Identifiable { var data: TaskData - var subtaskList: AsyncTaskList + var subtaskList: AsyncTaskList = AsyncTaskList([]) /* MARK: Initializers */ init(_ data: TaskData) { self.data = data - self.subtaskList = AsyncTaskList(data.subtasks) } init (creatorID: String) { @@ -42,10 +41,10 @@ class Task: Hashable { func finishEdit() { return } - func refreshTaskList() -> Task { - self.subtaskList = AsyncTaskList(self.data.subtasks) - return self - } +// func refreshTaskList() -> Task { +// self.subtaskList = AsyncTaskList(self.data.subtasks) +// return self +// } func changeName(_ name: String) -> Task { self.data = TaskData(copyOf: self.data, @@ -125,8 +124,9 @@ class Task: Hashable { return self } - func addSubtask(_ taskID: String) -> Task { - self.data.subtasks.append(taskID) + func addSubtask(_ task: Task) -> Task { + self.subtaskList.tasks.append(task) + self.data.subtasks.append(task.data.id) return self } diff --git a/process/Models/User.swift b/process/Models/User.swift index 73dfe58..e71e065 100644 --- a/process/Models/User.swift +++ b/process/Models/User.swift @@ -13,10 +13,13 @@ import SwiftUI /** Singleton class used as a functional intermediary between the UserData struct, which stores user data and models. */ class User: ObservableObject { - + + public static var user: User = User() + var data: UserData var profilePicture: UIImage = UIImage(named: ProfileConstant.defaultProfilePicture)! var taskList: AsyncTaskList + var projectList: [Project] = [] /* MARK: Initializers */ @@ -24,7 +27,7 @@ class User: ObservableObject { identifying information. */ init(_ data: UserData) { self.data = data - self.taskList = AsyncTaskList(data.tasks) + self.taskList = AsyncTaskList([]) } /** Only for placeholder models with no data, so the purpose of the @@ -67,22 +70,47 @@ class User: ObservableObject { return self } - func addTask(_ taskID: String) -> User { - if !self.data.tasks.contains(taskID) { - self.data.tasks.append(taskID) + func addTask(_ task: Task) -> User { + if !self.data.tasks.contains(task.data.id) { + self.data.tasks.append(task.data.id) + self.taskList.insertTask(task) + } + self.objectWillChange.send() + return self + } + + func addTaskToMyProject(_ task: Task, _ from: String?) -> User { + if from != nil { + for project in self.projectList { + if project.data.id == from! { + project.removeTask(task.data.id).finishEdit() + } + } + } + if task.data.project != nil { + let projectID: String = task.data.project! + for project in self.projectList { + print(project) + if project.data.id == projectID { + print(project.data.id) + project.addTask(task).finishEdit() + } + } } return self } func removeTask(_ taskID: String) -> User { self.data.tasks.removeAll { $0 == taskID } - self.taskList.items.removeAll { $0.task.data.id == taskID } + self.taskList.tasks.removeAll { $0.data.id == taskID } + self.objectWillChange.send() return self } - func addOwnedProject(_ projectID: String) -> User { - self.data.ownedProjects.append(projectID) - self.data.allProjects.append(projectID) + func addOwnedProject(_ project: Project) -> User { + self.data.ownedProjects.append(project.data.id) + self.data.allProjects.append(project.data.id) + self.projectList.append(project) return self } diff --git a/process/Support/Lifecycle/RootView.swift b/process/Support/Lifecycle/RootView.swift index b30f3f9..da5851f 100644 --- a/process/Support/Lifecycle/RootView.swift +++ b/process/Support/Lifecycle/RootView.swift @@ -12,7 +12,7 @@ import SwiftUI /** Root view managing access between verification and home views. */ struct RootView: View { - @ObservedObject var model = RootViewModel() + @StateObject var model = RootViewModel() /* MARK: Superview fork */ @@ -55,6 +55,7 @@ class RootViewModel: ObservableObject { APIHandler.pullAuthenticatedUser { user, error in guard error == nil && user != nil else { return } self.user = user! + User.user = self.user self.user.pullProfilePicture() { error, _ in guard error == nil else { return } self.userSignedIn = true diff --git a/process/Utilities/Classes/APIHandler.swift b/process/Utilities/Classes/APIHandler.swift index c819602..99425f2 100644 --- a/process/Utilities/Classes/APIHandler.swift +++ b/process/Utilities/Classes/APIHandler.swift @@ -399,15 +399,15 @@ class APIHandler { completion(error) return } - project? - .addTask(taskID) - .push { error in - guard error == nil else { - completion(error) - return - } - completion(nil) - } +// project? +// .addTask(taskID) +// .push { error in +// guard error == nil else { +// completion(error) +// return +// } +// completion(nil) +// } } } } diff --git a/process/Utilities/Classes/DueDateUtils.swift b/process/Utilities/Classes/DueDateUtils.swift index 2658dfb..3a619d2 100644 --- a/process/Utilities/Classes/DueDateUtils.swift +++ b/process/Utilities/Classes/DueDateUtils.swift @@ -152,8 +152,8 @@ class DueDateUtils { private static func getCompletedTasks(user: User) -> [Task] { var taskList: [Task] = [] var completedTasks: [Task] = [] - for taskItem in user.taskList.items { - taskList.append(taskItem.task) + for task in user.taskList.tasks { + taskList.append(task) } for task in taskList { if task.data.dateCompleted != nil {