mycode/myApp/TallyFlow/IOS/Features/Tasks/TasksTabView.swift
songyc macbook b209199c2d feat: Setup app shell, SwiftData models, and real-time tracking engine
- Initialize iOS project with 6-tab navigation structure
- Configure custom Light/Dark themes and AppColors
- Define SwiftData models for Tasks, Tags, Goals, and TrackingRecords
- Setup relationships (Super/Sub tags) and cascade delete rules
- Implement Observable TrackingEngine for real-time timer updates
2026-06-26 03:32:29 +09:00

261 lines
9.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import SwiftData
struct TasksTabView: View {
@Environment(\.modelContext) private var context
@Query(sort: \TaskItem.sortOrder) private var tasks: [TaskItem]
@State private var showAddSheet = false
@State private var editTarget: TaskItem?
private var activeTasks: [TaskItem] { tasks.filter { !$0.isArchived } }
var body: some View {
NavigationStack {
Group {
if activeTasks.isEmpty {
EmptyStateView(
icon: "checkmark.circle",
message: String(localized: "tasks.empty")
)
} else {
List {
ForEach(activeTasks) { task in
TaskRowView(task: task)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
context.delete(task)
} label: {
Label("삭제", systemImage: "trash")
}
Button {
editTarget = task
} label: {
Label("편집", systemImage: "pencil")
}
.tint(.blue)
}
}
}
.scrollContentBackground(.hidden)
}
}
.background(Color.tfBackground)
.navigationTitle(String(localized: "tab.tasks"))
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button { showAddSheet = true } label: {
Image(systemName: "plus")
}
.tint(Color.tfPrimary)
}
}
.sheet(isPresented: $showAddSheet) {
TaskFormView()
}
.sheet(item: $editTarget) { task in
TaskFormView(existing: task)
}
}
}
}
// MARK: Row
private struct TaskRowView: View {
let task: TaskItem
private var primaryColor: Color {
task.tags.first.map { Color(hex: $0.colorHex) } ?? Color.tfPrimary
}
var body: some View {
HStack(spacing: 12) {
Image(systemName: task.icon)
.foregroundStyle(primaryColor)
.frame(width: 28)
VStack(alignment: .leading, spacing: 2) {
Text(task.name)
.foregroundStyle(Color.tfOnBackground)
HStack(spacing: 4) {
Text(task.taskType == .timer ? "타이머" : "카운터")
.font(.caption)
.foregroundStyle(Color.tfMuted)
if let firstTag = task.tags.first {
Text("·")
.font(.caption)
.foregroundStyle(Color.tfMuted)
Text(firstTag.name)
.font(.caption)
.foregroundStyle(Color(hex: firstTag.colorHex))
}
}
}
Spacer()
Image(systemName: task.taskType == .timer ? "timer" : "number")
.foregroundStyle(primaryColor)
}
.padding(.vertical, 4)
}
}
// MARK: Form
struct TaskFormView: View {
@Environment(\.modelContext) private var context
@Environment(\.dismiss) private var dismiss
@Query(sort: \TagEntity.sortOrder) private var allTags: [TagEntity]
var existing: TaskItem? = nil
@State private var name = ""
@State private var icon = "star"
@State private var taskType: TaskType = .timer
@State private var selectedTagIDs: Set<UUID> = []
private let iconPresets = [
"star", "heart", "bolt", "flame",
"book.fill", "pencil", "laptopcomputer", "dumbbell.fill",
"music.note", "camera.fill", "gamecontroller.fill", "fork.knife",
"figure.walk", "bicycle", "car.fill", "airplane",
"moon.stars.fill", "sun.max.fill", "drop.fill", "leaf.fill"
]
private var isEditing: Bool { existing != nil }
private var isSaveDisabled: Bool { name.trimmingCharacters(in: .whitespaces).isEmpty }
var body: some View {
NavigationStack {
Form {
Section("이름") {
TextField("작업 이름", text: $name)
}
Section("아이콘") {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 12) {
ForEach(iconPresets, id: \.self) { sym in
Image(systemName: sym)
.font(.title2)
.foregroundStyle(sym == icon ? Color.tfPrimary : Color.tfMuted)
.frame(width: 44, height: 44)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(sym == icon ? Color.tfPrimary.opacity(0.15) : Color.clear)
)
.onTapGesture { icon = sym }
}
}
.padding(.vertical, 4)
}
Section("유형") {
Picker("유형", selection: $taskType) {
Text("타이머").tag(TaskType.timer)
Text("카운터").tag(TaskType.counter)
}
.pickerStyle(.segmented)
}
if !allTags.isEmpty {
Section("태그") {
ForEach(allTags) { tag in
TagSelectionRow(
tag: tag,
isSelected: selectedTagIDs.contains(tag.id),
onToggle: { toggleTag(tag) }
)
}
}
}
}
.navigationTitle(isEditing ? "작업 편집" : "작업 추가")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("취소") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("저장") { save() }
.disabled(isSaveDisabled)
}
}
.onAppear { loadExisting() }
}
}
private func loadExisting() {
guard let task = existing else { return }
name = task.name
icon = task.icon
taskType = task.taskType
selectedTagIDs = Set(task.tags.map { $0.id })
}
private func toggleTag(_ tag: TagEntity) {
if selectedTagIDs.contains(tag.id) {
selectedTagIDs.remove(tag.id)
} else {
selectedTagIDs.insert(tag.id)
// Selecting a sub tag implicitly includes its parent super tag
if let parent = tag.parent {
selectedTagIDs.insert(parent.id)
}
}
}
private func save() {
let trimmed = name.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
let selectedTags = allTags.filter { selectedTagIDs.contains($0.id) }
if let task = existing {
task.name = trimmed
task.icon = icon
task.taskType = taskType
task.tags = selectedTags
} else {
let newTask = TaskItem(name: trimmed, icon: icon, taskType: taskType)
newTask.tags = selectedTags
context.insert(newTask)
}
dismiss()
}
}
private struct TagSelectionRow: View {
let tag: TagEntity
let isSelected: Bool
let onToggle: () -> Void
var body: some View {
HStack(spacing: 8) {
if tag.isSubTag {
Spacer().frame(width: 14)
}
Circle()
.fill(Color(hex: tag.colorHex))
.frame(width: 10, height: 10)
Text(tag.name)
.foregroundStyle(Color.tfOnBackground)
if tag.isSubTag, let parent = tag.parent {
Text("(\(parent.name))")
.font(.caption)
.foregroundStyle(Color.tfMuted)
}
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundStyle(Color.tfPrimary)
}
}
.contentShape(Rectangle())
.onTapGesture { onToggle() }
}
}
#Preview {
TasksTabView()
.modelContainer(for: [TaskItem.self, TagEntity.self, TrackingRecord.self], inMemory: true)
.environment(AppPreferences())
}