mycode/myApp/Haru_Danim/IOS/ContentView.swift
songyc macbook afb7435656 feat: enhance records with charts/filters and update main tab UI
[New Features]
- feat(records): add line charts for weekly/monthly trends and averages
- feat(records): implement multi-select filters for specific tags and actions
- feat(main): add 'view records' option to the action icon long-press menu
- feat(main): display goal progress widget with customizable settings

[UI/UX & Bug Fixes]
- fix(ui): expand touchable areas globally, especially in record lists
- chore(main): rename main tab top title from '하루다님' to '모음'
- feat(nav): route to goal detail screen when tapping the goal widget
2026-07-09 02:22:04 +09:00

214 lines
6.5 KiB
Swift

//
// ContentView.swift
// Haru_Danim
//
// + (CLAUDE.md §5, )
//
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var context
@AppStorage(SettingsKeys.theme) private var theme: String = "light"
@State private var showSplash = true
var body: some View {
ZStack {
MainTabView()
if showSplash {
SplashView()
.transition(.opacity)
.zIndex(1)
}
}
.tint(AppTheme.green)
.preferredColorScheme(theme == "dark" ? .dark : .light)
.task {
#if DEBUG
DebugSeed.seedIfRequested(context: context)
DebugSeed.autoStartIfRequested(context: context)
#endif
try? await Task.sleep(for: .seconds(1.2))
withAnimation(.easeOut(duration: 0.4)) {
showSplash = false
}
}
}
}
// MARK: - ( + + , CLAUDE.md §5.1)
struct SplashView: View {
@State private var appeared = false
var body: some View {
ZStack {
// ( ) /
AppTheme.background.ignoresSafeArea()
VStack(spacing: 18) {
Image("AppLogo")
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 34, style: .continuous))
.shadow(color: .black.opacity(0.12), radius: 14, y: 6)
.scaleEffect(appeared ? 1 : 0.85)
.opacity(appeared ? 1 : 0)
Text("하루다님", comment: "앱 이름")
.font(.largeTitle.bold())
.foregroundStyle(AppTheme.green)
.opacity(appeared ? 1 : 0)
Text("당신의 하루에 다녀가다", comment: "스플래시 브랜드 멘트")
.font(.subheadline)
.foregroundStyle(.secondary)
.opacity(appeared ? 1 : 0)
}
}
.onAppear {
withAnimation(.spring(duration: 0.5)) {
appeared = true
}
}
}
}
// MARK: -
enum AppTab: String, CaseIterable, Identifiable {
case main, action, tag, goal, history, settings
var id: String { rawValue }
var label: String {
switch self {
case .main: return "모음"
case .action: return "행동"
case .tag: return "꼬리표"
case .goal: return "목표"
case .history: return "기록"
case .settings: return "설정"
}
}
var symbol: String {
switch self {
case .main: return "square.grid.2x2.fill"
case .action: return "figure.walk"
case .tag: return "tag.fill"
case .goal: return "flag.checkered"
case .history: return "calendar"
case .settings: return "gearshape.fill"
}
}
/// ("main,action,history")
static func visibleTabs(from raw: String) -> [AppTab] {
let chosen = Set(raw.split(separator: ",").map(String.init))
let tabs = AppTab.allCases.filter { chosen.contains($0.rawValue) }
return tabs.isEmpty ? [.main] : Array(tabs.prefix(TabBarConfig.maxVisible))
}
@ViewBuilder
var rootView: some View {
switch self {
case .main: MainView()
case .action: ActionListView()
case .tag: TagListView()
case .goal: GoalListView()
case .history: HistoryView()
case .settings: SettingsView()
}
}
}
// MARK: - ( 1~3 + , = )
struct MainTabView: View {
@AppStorage(SettingsKeys.visibleTabs) private var visibleTabsRaw = TabBarConfig.defaultVisibleTabs
private static let moreTabValue = "more"
@State private var selection: String = {
#if DEBUG
if let raw = UserDefaults.standard.string(forKey: "startTab") {
return raw
}
#endif
return AppTab.main.rawValue
}()
private var visibleTabs: [AppTab] {
AppTab.visibleTabs(from: visibleTabsRaw)
}
private var moreTabs: [AppTab] {
AppTab.allCases.filter { !visibleTabs.contains($0) }
}
var body: some View {
TabView(selection: $selection) {
ForEach(visibleTabs) { tab in
Tab(tab.label, systemImage: tab.symbol, value: tab.rawValue) {
NavigationStack {
tab.rootView
}
}
}
Tab("더보기", systemImage: "ellipsis", value: Self.moreTabValue) {
MoreTabView(tabs: moreTabs)
}
}
.onChange(of: visibleTabsRaw) {
//
let valid = visibleTabs.map(\.rawValue) + [Self.moreTabValue]
if !valid.contains(selection) {
selection = Self.moreTabValue
}
}
.onAppear {
#if DEBUG
// : -startTab
let valid = visibleTabs.map(\.rawValue) + [Self.moreTabValue]
if !valid.contains(selection) {
selection = Self.moreTabValue
}
#endif
}
}
}
// MARK: -
struct MoreTabView: View {
let tabs: [AppTab]
var body: some View {
NavigationStack {
List(tabs) { tab in
NavigationLink {
tab.rootView
} label: {
Label {
Text(tab.label)
} icon: {
Image(systemName: tab.symbol)
.foregroundStyle(AppTheme.green)
}
}
}
.scrollContentBackground(.hidden)
.background(AppTheme.background)
.navigationTitle("더보기")
}
}
}
#Preview {
ContentView()
.modelContainer(for: [
Tag.self, Action.self, TimeSession.self,
CountEntry.self, Goal.self, Quest.self,
], inMemory: true)
}