- Implement Goal(목표) and Quest(다짐) functionality - Add a new feature for tracking and recording history - TODO: Fix an issue in 횟수측정 Action(행동) where count and time are recorded but not reflected in the icon
147 lines
4.5 KiB
Swift
147 lines
4.5 KiB
Swift
//
|
|
// GoalTabView.swift
|
|
// HaruDanim
|
|
//
|
|
// Lists all `Goal`s and hosts a sheet for creating new ones.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct GoalTabView: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Query(sort: \Goal.createdAt, order: .reverse) private var goals: [Goal]
|
|
|
|
@State private var isAddSheetPresented = false
|
|
|
|
private var dateRangeFormatter: Date.FormatStyle {
|
|
.dateTime.year().month().day()
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
ForEach(goals) { goal in
|
|
NavigationLink {
|
|
GoalDetailView(goal: goal)
|
|
} label: {
|
|
HStack(spacing: 12) {
|
|
Circle()
|
|
.fill(Color(hex: goal.colorHex))
|
|
.frame(width: 20, height: 20)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(goal.title)
|
|
Text("\(goal.startDate.formatted(dateRangeFormatter)) ~ \(goal.endDate.formatted(dateRangeFormatter))")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Text("다짐 \(goal.quests.count)")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.onDelete(perform: deleteGoals)
|
|
}
|
|
.navigationTitle("목표")
|
|
.overlay {
|
|
if goals.isEmpty {
|
|
ContentUnavailableView(
|
|
"목표가 없어요",
|
|
systemImage: "flag",
|
|
description: Text("오른쪽 위 + 버튼으로 목표를 추가하세요.")
|
|
)
|
|
}
|
|
}
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button {
|
|
isAddSheetPresented = true
|
|
} label: {
|
|
Label("목표 추가", systemImage: "plus")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $isAddSheetPresented) {
|
|
AddGoalSheet()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func deleteGoals(at offsets: IndexSet) {
|
|
for index in offsets {
|
|
modelContext.delete(goals[index])
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Add Goal Sheet
|
|
|
|
private struct AddGoalSheet: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var title = ""
|
|
@State private var color = Color(hex: "#2F6B4F")
|
|
@State private var startDate = Date.now
|
|
@State private var endDate = Date.now
|
|
|
|
private var trimmedTitle: String {
|
|
title.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
private var isValid: Bool {
|
|
!trimmedTitle.isEmpty && endDate >= startDate
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section("이름") {
|
|
TextField("목표 이름", text: $title)
|
|
}
|
|
|
|
Section("색상") {
|
|
ColorPicker("색상 선택", selection: $color, supportsOpacity: false)
|
|
}
|
|
|
|
Section("기간") {
|
|
DatePicker("시작일", selection: $startDate, displayedComponents: .date)
|
|
DatePicker("종료일", selection: $endDate, in: startDate..., displayedComponents: .date)
|
|
}
|
|
}
|
|
.navigationTitle("새 목표")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("취소") { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("저장") { save() }
|
|
.disabled(!isValid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func save() {
|
|
let goal = Goal(
|
|
title: trimmedTitle,
|
|
colorHex: color.toHex(),
|
|
startDate: startDate,
|
|
endDate: endDate
|
|
)
|
|
modelContext.insert(goal)
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
GoalTabView()
|
|
.modelContainer(for: [Goal.self, Quest.self, Action.self, Tag.self], inMemory: true)
|
|
}
|