177 lines
5.4 KiB
Swift
177 lines
5.4 KiB
Swift
//
|
|
// ContentView.swift
|
|
// Haru_Danim
|
|
//
|
|
// 스플래시 + 6개 탭 구조 (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("하루 다님")
|
|
.font(.largeTitle.bold())
|
|
.foregroundStyle(AppTheme.green)
|
|
.opacity(appeared ? 1 : 0)
|
|
Text("하루 습관 및 시간 추적")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.opacity(appeared ? 1 : 0)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.spring(duration: 0.5)) {
|
|
appeared = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 탭 구조 (총 6개, 순서 고정 — 시스템 탭바는 5개 초과 시 접히므로 커스텀 탭바 사용)
|
|
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MainTabView: View {
|
|
@State private var selection: AppTab = {
|
|
#if DEBUG
|
|
if let raw = UserDefaults.standard.string(forKey: "startTab"),
|
|
let tab = AppTab(rawValue: raw) {
|
|
return tab
|
|
}
|
|
#endif
|
|
return .main
|
|
}()
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
switch selection {
|
|
case .main: MainView()
|
|
case .action: ActionListView()
|
|
case .tag: TagListView()
|
|
case .goal: GoalListView()
|
|
case .history: HistoryView()
|
|
case .settings: SettingsView()
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.safeAreaInset(edge: .bottom) {
|
|
tabBar
|
|
}
|
|
.background(AppTheme.background)
|
|
}
|
|
|
|
/// 리퀴드 글라스 스타일의 플로팅 탭바
|
|
private var tabBar: some View {
|
|
HStack(spacing: 0) {
|
|
ForEach(AppTab.allCases) { tab in
|
|
Button {
|
|
withAnimation(.spring(duration: 0.25)) {
|
|
selection = tab
|
|
}
|
|
} label: {
|
|
VStack(spacing: 3) {
|
|
Image(systemName: tab.symbol)
|
|
.font(.system(size: 19, weight: .medium))
|
|
Text(tab.label)
|
|
.font(.system(size: 10, weight: .semibold))
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 8)
|
|
.background {
|
|
if selection == tab {
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
.fill(AppTheme.green.opacity(0.16))
|
|
}
|
|
}
|
|
.foregroundStyle(selection == tab ? AppTheme.green : Color.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(6)
|
|
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 26, style: .continuous))
|
|
.padding(.horizontal, 10)
|
|
.padding(.bottom, 4)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ContentView()
|
|
.modelContainer(for: [
|
|
Tag.self, Action.self, TimeSession.self,
|
|
CountEntry.self, Goal.self, Quest.self,
|
|
], inMemory: true)
|
|
}
|