mycode/myApp/TallyFlow/IOS/Features/Main/MainTabView.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

218 lines
6.7 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
// Module-level so GoalsTabView (same module) can reuse this formatter.
extension TimeInterval {
var hhmmss: String {
let s = Int(self)
return String(format: "%02d:%02d:%02d", s / 3600, (s % 3600) / 60, s % 60)
}
}
struct MainTabView: View {
@Query(sort: \TaskItem.sortOrder) private var tasks: [TaskItem]
@State private var engine = TrackingEngine()
private var activeTasks: [TaskItem] { tasks.filter { !$0.isArchived } }
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 20) {
SummaryCardsSection()
if !activeTasks.isEmpty {
TaskListSection(tasks: activeTasks)
}
LayoutEditEntryBanner()
}
.padding()
}
.background(Color.tfBackground)
.navigationTitle(String(localized: "tab.main"))
}
.environment(engine)
}
}
// MARK: Summary cards
private struct SummaryCardsSection: View {
var body: some View {
VStack(spacing: 12) {
SummaryCard(period: String(localized: "main.period.today"), icon: "sun.max.fill")
SummaryCard(period: String(localized: "main.period.week"), icon: "calendar.badge.clock")
SummaryCard(period: String(localized: "main.period.month"), icon: "calendar")
}
}
}
private struct SummaryCard: View {
let period: String
let icon: String
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.title3)
.foregroundStyle(Color.tfPrimary)
.frame(width: 28)
Text(period)
.font(.headline)
.foregroundStyle(Color.tfOnBackground)
Spacer()
Text("")
.font(.subheadline)
.foregroundStyle(Color.tfMuted)
}
.padding()
.background(Color.tfSurface)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: Color.tfSeparator, radius: 2, x: 0, y: 1)
}
}
// MARK: Task list
private struct TaskListSection: View {
let tasks: [TaskItem]
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("작업")
.font(.footnote.weight(.semibold))
.foregroundStyle(Color.tfMuted)
.textCase(.uppercase)
ForEach(tasks) { task in
TaskActionCard(task: task)
}
}
}
}
private struct TaskActionCard: View {
@Environment(\.modelContext) private var context
@Environment(TrackingEngine.self) private var engine
let task: TaskItem
@State private var counterPulse = false
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)
.font(.title3)
.foregroundStyle(primaryColor)
.frame(width: 28)
VStack(alignment: .leading, spacing: 2) {
Text(task.name)
.font(.subheadline.weight(.semibold))
.foregroundStyle(Color.tfOnBackground)
Text(statusLabel)
.font(.caption.monospacedDigit())
.foregroundStyle(engine.isRunning(task) ? primaryColor : Color.tfMuted)
.contentTransition(.numericText())
.animation(.linear(duration: 0.2), value: statusLabel)
}
Spacer()
actionButton
}
.padding()
.background(Color.tfSurface)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: Color.tfSeparator, radius: 2, x: 0, y: 1)
}
private var statusLabel: String {
switch task.taskType {
case .timer:
return engine.isRunning(task)
? engine.elapsed(for: task).hhmmss
: String(localized: "task.type.timer")
case .counter:
return String(localized: "task.type.counter")
}
}
@ViewBuilder
private var actionButton: some View {
switch task.taskType {
case .timer:
let running = engine.isRunning(task)
Button { toggleTimer() } label: {
Image(systemName: running ? "stop.fill" : "play.fill")
.font(.title3)
.foregroundStyle(.white)
.frame(width: 44, height: 44)
.background(running ? Color.red : primaryColor)
.clipShape(Circle())
}
.buttonStyle(.plain)
case .counter:
Button { logCount() } label: {
Image(systemName: "plus")
.font(.title3)
.foregroundStyle(.white)
.frame(width: 44, height: 44)
.background(primaryColor)
.clipShape(Circle())
.scaleEffect(counterPulse ? 1.3 : 1.0)
}
.buttonStyle(.plain)
}
}
private func toggleTimer() {
if engine.isRunning(task) {
engine.stopTimer(for: task, context: context)
} else {
engine.startTimer(for: task, context: context)
}
}
private func logCount() {
engine.incrementCounter(for: task, context: context)
withAnimation(.spring(duration: 0.15, bounce: 0.6)) { counterPulse = true }
Task {
try? await Task.sleep(for: .milliseconds(350))
withAnimation(.spring(duration: 0.2)) { counterPulse = false }
}
}
}
// MARK: Layout-edit mode entry
private struct LayoutEditEntryBanner: View {
var body: some View {
HStack(spacing: 10) {
Image(systemName: "square.grid.2x2")
.foregroundStyle(Color.tfAccent)
Text(String(localized: "main.layout_edit.placeholder"))
.font(.footnote)
.foregroundStyle(Color.tfMuted)
Spacer()
Image(systemName: "chevron.right")
.font(.footnote.weight(.semibold))
.foregroundStyle(Color.tfMuted)
}
.padding()
.background(Color.tfSurface)
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.tfSeparator, lineWidth: 1)
)
}
}
#Preview {
MainTabView()
.modelContainer(for: [TaskItem.self, TagEntity.self, TrackingRecord.self], inMemory: true)
.environment(AppPreferences())
}