A TaskItem can be scheduled for a certain time of day. I'd like to mark it as complete for that time of day. I have the below solution, but if I have more than one of the same TaskRow(TaskItem, Date, TimeOfDay) and I update one, the other doesn't update dynamically.
TaskRow:
import SwiftData
import SwiftUI
struct TaskRow: View {
let task: TaskItem
let date: Date
let timeOfDay: TimeOfDay
@Environment(\.modelContext) private var modelContext
@State private var isCompleted: Bool
init(task: TaskItem, date: Date, timeOfDay: TimeOfDay) {
self.task = task
self.date = date
self.timeOfDay = timeOfDay
_isCompleted = State(
initialValue: task.isCompleted(for: date, timeOfDay: timeOfDay))
}
var body: some View {
HStack {
Image(systemName: isCompleted ? "checkmark.square" : "square")
.foregroundColor(isCompleted ? .blue : .gray)
.onTapGesture {
isCompleted.toggle()
if isCompleted {
let completion = TaskCompletion(
date: date, timeOfDay: timeOfDay, taskItem: task)
modelContext.insert(completion)
} else {
if let completion = task.completions.first(where: {
$0.date == Calendar.current.startOfDay(for: date)
&& $0.timeOfDay == timeOfDay
}) {
modelContext.delete(completion)
}
}
try? modelContext.save()
}
Text(task.taskDescription ?? "No description available")
Spacer()
}
.contentShape(Rectangle())
}
}
TaskCompletion:
import SwiftData
import Foundation
@Model
class TaskCompletion {
var date: Date
var timeOfDay: TimeOfDay
@Relationship(inverse: \TaskItem.completions) var taskItem: TaskItem
init(date: Date, timeOfDay: TimeOfDay, taskItem: TaskItem) {
let calendar = Calendar.current
self.date = calendar.startOfDay(for: date)
self.timeOfDay = timeOfDay
self.taskItem = taskItem
}
}
enum TimeOfDay: String, Codable, CaseIterable {
case firstThing, morning, midDay, afternoon, evening
var displayName: String {
switch self {
case .firstThing: return "First Thing in the Morning"
case .morning: return "Morning"
case .midDay: return "Mid Day"
case .afternoon: return "Afternoon"
case .evening: return "Evening"
}
}
}
TaskItem:
import Foundation
import SwiftData
@Model
class TaskItem {
// Common properties
var uuid: UUID = UUID()
var startDate: Date
var schedule: Set<ScheduleCombination>
@Relationship(deleteRule: .cascade) var completions: [TaskCompletion] = []
@Relationship(deleteRule: .cascade) var reflections: [TaskReflection] = []
// From TaskItem
var taskDescription: String? = nil
@Relationship(inverse: \Blueprint.tasks) var blueprint: Blueprint?
// From TaskSuggestion (primaryGoal is now non-optional)
var primaryGoal: PrimaryGoal = PrimaryGoal.health // Default value
var areaOfImprovement: AreasOfImprovement? = nil
var easyDescription: String? = nil
var mediumDescription: String? = nil
var hardDescription: String? = nil
var suggestionType: TaskSuggestionType = TaskSuggestionType.other
var difficulty: TaskDifficulty? = nil
var isPrimaryGoal: Bool = false
var isAreaOfImprovement: Bool = false
init(
taskDescription: String? = nil,
startDate: Date = Date(),
schedule: Set<ScheduleCombination>,
primaryGoal: PrimaryGoal = .health,
blueprint: Blueprint? = nil,
areaOfImprovement: AreasOfImprovement? = nil,
easyDescription: String? = nil,
mediumDescription: String? = nil,
hardDescription: String? = nil,
suggestionType: TaskSuggestionType = TaskSuggestionType.other,
difficulty: TaskDifficulty? = nil,
isPrimaryGoal: Bool = false,
isAreaOfImprovement: Bool = false,
uuid: UUID = UUID()
) {
self.taskDescription = taskDescription
self.startDate = startDate
self.schedule = schedule
self.primaryGoal = primaryGoal
self.blueprint = blueprint
self.areaOfImprovement = areaOfImprovement
self.easyDescription = easyDescription
self.mediumDescription = mediumDescription
self.hardDescription = hardDescription
self.suggestionType = suggestionType
self.difficulty = difficulty
self.isPrimaryGoal = isPrimaryGoal
self.isAreaOfImprovement = isAreaOfImprovement
self.uuid = uuid
}
// Convenience initializer to match TaskCreator calls
convenience init(
primaryGoal: PrimaryGoal, // Non-optional
schedule: Set<ScheduleCombination>,
easyDescription: String,
mediumDescription: String,
hardDescription: String,
suggestionType: TaskSuggestionType,
difficulty: TaskDifficulty
) {
self.init(
taskDescription: nil,
startDate: Date(),
schedule: schedule,
primaryGoal: primaryGoal,
blueprint: nil,
areaOfImprovement: nil,
easyDescription: easyDescription,
mediumDescription: mediumDescription,
hardDescription: hardDescription,
suggestionType: suggestionType,
difficulty: difficulty,
isPrimaryGoal: suggestionType == .primaryGoal,
isAreaOfImprovement: suggestionType == .areaOfImprovement
)
}
// Scheduling and completion methods (unchanged)
func isScheduled(for day: DayOfWeek, timeOfDay: TimeOfDay) -> Bool {
return schedule.contains(
ScheduleCombination(dayOfWeek: day, timeOfDay: timeOfDay))
}
func isCompleted(for date: Date, timeOfDay: TimeOfDay) -> Bool {
let calendar = Calendar.current
let startOfDay = calendar.startOfDay(for: date)
return completions.contains {
$0.date == startOfDay && $0.timeOfDay == timeOfDay
}
}
func markCompleted(for date: Date, timeOfDay: TimeOfDay) {
if !isCompleted(for: date, timeOfDay: timeOfDay) {
let newCompletion = TaskCompletion(
date: date, timeOfDay: timeOfDay, taskItem: self)
completions.append(newCompletion)
}
}
}
enum DayOfWeek: String, Codable, CaseIterable {
case monday, tuesday, wednesday, thursday, friday, saturday, sunday
}
extension Date {
var dayOfWeek: DayOfWeek {
let calendar = Calendar.current
let weekday = calendar.component(.weekday, from: self)
let index = (weekday - 2) % 7
return DayOfWeek.allCases[index]
}
}
struct ScheduleCombination: Hashable, Codable {
var dayOfWeek: DayOfWeek
var timeOfDay: TimeOfDay
}
enum TaskSuggestionType: String, Codable {
case primaryGoal
case areaOfImprovement
case outcome
case other
}
enum TaskDifficulty: String, Codable, CaseIterable, Identifiable {
case foundational
case intermediate
case challenging
var id: String { rawValue }
}
Removing the init and updating the state in .onAppear seems to have fixed the issue
import SwiftData
import SwiftUI
struct TaskRow: View {
let task: TaskItem
let date: Date
let timeOfDay: TimeOfDay
let showCompletionStatus: Bool
@Environment(\.modelContext) private var modelContext
@State private var isCompletedState: Bool = false
var body: some View {
HStack {
Image(systemName: isCompletedState ? "checkmark.square" : "square")
.foregroundColor(isCompletedState ? .blue : .gray)
.onAppear {
isCompletedState = task.isCompleted(for: date, timeOfDay: timeOfDay)
}
.onTapGesture {
toggleCompletion()
}
Text(task.taskDescription ?? "No description available")
Spacer()
if showCompletionStatus {
Text(isCompletedState ? "Completed" : "Not completed")
.font(.subheadline)
.foregroundColor(isCompletedState ? .green : .red)
.padding(.leading, 4)
.onTapGesture {
toggleCompletion()
}
}
}
.contentShape(Rectangle())
}
private func toggleCompletion() {
let startOfDay = Calendar.current.startOfDay(for: date)
if isCompletedState {
if let completion = task.completions.first(where: {
$0.date == startOfDay && $0.timeOfDay == timeOfDay
}) {
modelContext.delete(completion)
}
} else {
task.markCompleted(for: date, timeOfDay: timeOfDay)
}
try? modelContext.save()
withAnimation(.easeInOut) {
isCompletedState = task.isCompleted(for: date, timeOfDay: timeOfDay)
}
}
}