remove TallyFlow Project

This commit is contained in:
songyc macbook 2026-07-03 21:58:43 +09:00
parent b209199c2d
commit 1fe3386d55
30 changed files with 0 additions and 2941 deletions

View File

@ -1,63 +0,0 @@
# TallyFlow
## Product summary
TallyFlow is an iOS productivity tracker for measuring tasks by either time or count.
It supports hierarchical tags (super tag / sub tag), flexible goals, historical statistics, widgets, Live Activities, and a paired watchOS experience.
## Source root
- iOS source root folder: `IOS/`
- Do not assume the default inner app folder name is `TallyFlow`; use `IOS/` as the app source root.
## Non-negotiable constraints
- Use SwiftUI.
- Use SwiftData as the local persistence layer.
- Keep the project buildable after every milestone.
- Do not scan or refactor the entire project.
- Read only the files explicitly listed in each prompt.
- Edit only the files explicitly listed in each prompt.
- Do not mention competitor app names in code comments, docs, or commit messages.
- Korean is the default app language; English must be supported.
- Theme must support Light and Dark modes with custom green/yellow palettes, not generic system default vibes.
- All date aggregation must respect configurable week start day and configurable custom day start time.
- Time tracking must support multiple concurrent running timer tasks.
- A task can have multiple tags.
- Selecting a sub tag implies its parent super tag is also associated logically.
## Architecture rules
- Separate app shell, domain models, services, and feature views.
- Create a single source of truth for:
1. task definitions,
2. tag hierarchy,
3. goals,
4. tracking sessions / count logs,
5. calendar boundary calculations.
- Avoid business logic inside SwiftUI view bodies.
- Put theme colors/tokens in dedicated files.
- Keep milestone 1 free of widget/watch/live activity target work; only prepare the architecture so those can be added later.
## UX rules
- Tab order must be:
1. Main
2. Tasks
3. Tags
4. Goals
5. Stats
6. Settings
- Add a splash/loading screen that shows the app name.
- Main tab should be scaffolded for:
- today / week / month summary cards,
- task action buttons,
- future layout-edit mode.
- Settings must be scaffolded for:
- theme mode,
- language,
- week start day,
- custom day start time,
- premium section entry.
## Coding style
- Prefer small SwiftUI views.
- Prefer explicit model names over vague names.
- Add TODO markers only where the next milestone will extend logic.
- Do not add placeholder lorem ipsum text.
- Do not introduce third-party dependencies in milestone 1.

View File

@ -1,37 +0,0 @@
import SwiftData
import SwiftUI
struct RootTabView: View {
var body: some View {
TabView {
Tab(String(localized: "tab.main"), systemImage: "house.fill") {
MainTabView()
}
Tab(String(localized: "tab.tasks"), systemImage: "checkmark.circle.fill") {
TasksTabView()
}
Tab(String(localized: "tab.tags"), systemImage: "tag.fill") {
TagsTabView()
}
Tab(String(localized: "tab.goals"), systemImage: "target") {
GoalsTabView()
}
Tab(String(localized: "tab.stats"), systemImage: "chart.bar.fill") {
StatsTabView()
}
Tab(String(localized: "tab.settings"), systemImage: "gearshape.fill") {
SettingsTabView()
}
}
.tint(Color.tfPrimary)
}
}
#Preview {
RootTabView()
.modelContainer(
for: [TaskItem.self, TagEntity.self, GoalEntity.self, TrackingRecord.self],
inMemory: true
)
.environment(AppPreferences())
}

View File

@ -1,25 +0,0 @@
import SwiftUI
struct SplashView: View {
var body: some View {
ZStack {
Color.tfBackground.ignoresSafeArea()
VStack(spacing: 14) {
Image(systemName: "chart.bar.fill")
.font(.system(size: 60, weight: .semibold))
.foregroundStyle(Color.tfPrimary)
Text("TallyFlow")
.font(.system(size: 38, weight: .bold, design: .rounded))
.foregroundStyle(Color.tfOnBackground)
Text(String(localized: "splash.tagline"))
.font(.subheadline)
.foregroundStyle(Color.tfMuted)
}
}
}
}
#Preview {
SplashView()
.environment(AppPreferences())
}

View File

@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,35 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,42 +0,0 @@
//
// ContentView.swift
// TallyFlow
//
// Created by on 6/26/26.
//
import SwiftData
import SwiftUI
struct ContentView: View {
@Environment(AppPreferences.self) private var preferences
@State private var isShowingSplash = true
var body: some View {
Group {
if isShowingSplash {
SplashView()
.transition(.opacity)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) {
withAnimation(.easeOut(duration: 0.4)) {
isShowingSplash = false
}
}
}
} else {
RootTabView()
.transition(.opacity)
}
}
.preferredColorScheme(preferences.themeMode.colorScheme)
}
}
#Preview {
ContentView()
.modelContainer(
for: [TaskItem.self, TagEntity.self, GoalEntity.self, TrackingRecord.self],
inMemory: true
)
.environment(AppPreferences())
}

View File

@ -1,73 +0,0 @@
import Foundation
import SwiftData
enum GoalUnit: String, Codable, CaseIterable {
case seconds
case count
}
enum GoalPeriod: String, Codable, CaseIterable {
case daily
case weekly
case monthly
}
// Cannot be stored directly in SwiftData; use targetType computed property on GoalEntity.
enum TargetType {
case duration(min: Double, max: Double?)
case count(min: Int, max: Int?)
}
@Model
final class GoalEntity {
var id: UUID
var title: String
var targetValue: Double
var targetMax: Double?
var unit: GoalUnit
var period: GoalPeriod
var createdAt: Date
var isActive: Bool
var task: TaskItem?
var tag: TagEntity?
var targetType: TargetType {
switch unit {
case .seconds:
return .duration(min: targetValue, max: targetMax)
case .count:
return .count(min: Int(targetValue), max: targetMax.map(Int.init))
}
}
init(
id: UUID = UUID(),
title: String,
targetValue: Double,
targetMax: Double? = nil,
unit: GoalUnit = .seconds,
period: GoalPeriod = .daily,
createdAt: Date = .now,
isActive: Bool = true
) {
self.id = id
self.title = title
self.targetValue = targetValue
self.targetMax = targetMax
self.unit = unit
self.period = period
self.createdAt = createdAt
self.isActive = isActive
}
}
extension GoalPeriod {
var localizedName: String {
switch self {
case .daily: return String(localized: "goal.period.daily")
case .weekly: return String(localized: "goal.period.weekly")
case .monthly: return String(localized: "goal.period.monthly")
}
}
}

View File

@ -1,35 +0,0 @@
import Foundation
import SwiftData
@Model
final class TagEntity {
var id: UUID
var name: String
var colorHex: String
var createdAt: Date
var sortOrder: Int
var parent: TagEntity?
@Relationship(deleteRule: .cascade, inverse: \TagEntity.parent)
var subTags: [TagEntity] = []
// Inverse is declared on TaskItem.tags
var tasks: [TaskItem] = []
var isSubTag: Bool { parent != nil }
init(
id: UUID = UUID(),
name: String,
colorHex: String = "#52A878",
createdAt: Date = .now,
sortOrder: Int = 0
) {
self.id = id
self.name = name
self.colorHex = colorHex
self.createdAt = createdAt
self.sortOrder = sortOrder
}
}

View File

@ -1,42 +0,0 @@
import Foundation
import SwiftData
enum TaskType: String, Codable, CaseIterable {
case timer
case counter
}
@Model
final class TaskItem {
var id: UUID
var name: String
var icon: String
var taskType: TaskType
var createdAt: Date
var isArchived: Bool
var sortOrder: Int
@Relationship(deleteRule: .nullify, inverse: \TagEntity.tasks)
var tags: [TagEntity] = []
@Relationship(deleteRule: .cascade, inverse: \TrackingRecord.task)
var trackingRecords: [TrackingRecord] = []
init(
id: UUID = UUID(),
name: String,
icon: String = "star",
taskType: TaskType = .timer,
createdAt: Date = .now,
isArchived: Bool = false,
sortOrder: Int = 0
) {
self.id = id
self.name = name
self.icon = icon
self.taskType = taskType
self.createdAt = createdAt
self.isArchived = isArchived
self.sortOrder = sortOrder
}
}

View File

@ -1,43 +0,0 @@
import Foundation
import SwiftData
enum TrackingRecordType: String, Codable {
case timerSession
case countLog
}
@Model
final class TrackingRecord {
var id: UUID
var recordType: TrackingRecordType
var startTime: Date?
var endTime: Date?
var timestamp: Date?
var duration: TimeInterval
var createdAt: Date
// Inverse is declared on TaskItem.trackingRecords
var task: TaskItem?
var isRunning: Bool { recordType == .timerSession && startTime != nil && endTime == nil }
init(
id: UUID = UUID(),
task: TaskItem? = nil,
recordType: TrackingRecordType,
startTime: Date? = nil,
endTime: Date? = nil,
timestamp: Date? = nil,
duration: TimeInterval = 0,
createdAt: Date = .now
) {
self.id = id
self.task = task
self.recordType = recordType
self.startTime = startTime
self.endTime = endTime
self.timestamp = timestamp
self.duration = duration
self.createdAt = createdAt
}
}

View File

@ -1,46 +0,0 @@
import Foundation
final class CalendarBoundaryService {
private let preferences: AppPreferences
init(preferences: AppPreferences) {
self.preferences = preferences
}
private var calendar: Calendar {
var cal = Calendar.current
cal.firstWeekday = preferences.weekStartDay
return cal
}
func customDayStart(for date: Date) -> Date {
// TODO: apply dayStartHour / dayStartMinute boundary shift in milestone 2
var comps = calendar.dateComponents([.year, .month, .day], from: date)
comps.hour = preferences.dayStartHour
comps.minute = preferences.dayStartMinute
comps.second = 0
return calendar.date(from: comps) ?? date
}
func dayRange(for date: Date) -> DateInterval {
let start = customDayStart(for: date)
let end = calendar.date(byAdding: .day, value: 1, to: start) ?? start
return DateInterval(start: start, end: end)
}
func weekRange(for date: Date) -> DateInterval {
// TODO: align to custom day-start boundary in milestone 2
guard let interval = calendar.dateInterval(of: .weekOfYear, for: date) else {
return DateInterval(start: date, duration: 7 * 86_400)
}
return interval
}
func monthRange(for date: Date) -> DateInterval {
// TODO: align to custom day-start boundary in milestone 2
guard let interval = calendar.dateInterval(of: .month, for: date) else {
return DateInterval(start: date, duration: 30 * 86_400)
}
return interval
}
}

View File

@ -1,62 +0,0 @@
import Foundation
import SwiftData
@MainActor
@Observable
final class TrackingEngine {
private(set) var activeSessions: [UUID: Date] = [:]
private(set) var elapsedTimes: [UUID: TimeInterval] = [:]
private var tickTask: Task<Void, Never>?
init() {
tickTask = Task {
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(1))
self.tick()
}
}
}
private func tick() {
let now = Date.now
for (id, start) in activeSessions {
elapsedTimes[id] = now.timeIntervalSince(start)
}
}
func isRunning(_ task: TaskItem) -> Bool {
activeSessions[task.id] != nil
}
func elapsed(for task: TaskItem) -> TimeInterval {
elapsedTimes[task.id] ?? 0
}
func startTimer(for task: TaskItem, context: ModelContext) {
guard !isRunning(task) else { return }
let now = Date.now
let record = TrackingRecord(task: task, recordType: .timerSession, startTime: now)
context.insert(record)
task.trackingRecords.append(record)
activeSessions[task.id] = now
elapsedTimes[task.id] = 0
}
func stopTimer(for task: TaskItem, context: ModelContext) {
guard let startTime = activeSessions[task.id] else { return }
let now = Date.now
if let record = task.trackingRecords.first(where: { $0.isRunning }) {
record.endTime = now
record.duration = now.timeIntervalSince(startTime)
}
activeSessions.removeValue(forKey: task.id)
elapsedTimes.removeValue(forKey: task.id)
}
func incrementCounter(for task: TaskItem, context: ModelContext) {
let record = TrackingRecord(task: task, recordType: .countLog, timestamp: .now)
context.insert(record)
task.trackingRecords.append(record)
}
}

View File

@ -1,248 +0,0 @@
import SwiftUI
import SwiftData
struct GoalsTabView: View {
@Query(sort: \GoalEntity.createdAt) private var goals: [GoalEntity]
@State private var showAddSheet = false
var body: some View {
NavigationStack {
Group {
if goals.isEmpty {
EmptyStateView(
icon: "target",
message: String(localized: "goals.empty")
)
} else {
List(goals) { goal in
GoalRowView(goal: goal)
}
.scrollContentBackground(.hidden)
}
}
.background(Color.tfBackground)
.navigationTitle(String(localized: "tab.goals"))
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button { showAddSheet = true } label: {
Image(systemName: "plus")
}
.tint(Color.tfPrimary)
}
}
.sheet(isPresented: $showAddSheet) {
AddGoalSheet()
}
}
}
}
// MARK: Goal row
private struct GoalRowView: View {
let goal: GoalEntity
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(goal.title)
.foregroundStyle(Color.tfOnBackground)
HStack(spacing: 4) {
Text(goal.period.localizedName)
Text("·")
Text(targetDescription)
}
.font(.caption)
.foregroundStyle(Color.tfMuted)
}
Spacer()
Text(goal.isActive
? String(localized: "goal.active")
: String(localized: "goal.inactive"))
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(
goal.isActive
? Color.tfPrimary.opacity(0.15)
: Color.tfMuted.opacity(0.15)
)
.foregroundStyle(goal.isActive ? Color.tfPrimary : Color.tfMuted)
.clipShape(Capsule())
}
.padding(.vertical, 4)
}
private var targetDescription: String {
switch goal.targetType {
case .duration(let min, let max):
if let max {
return "\(min.hhmmss) \(max.hhmmss)"
}
return min.hhmmss
case .count(let min, let max):
if let max { return "\(min) \(max)" }
return "\(min)"
}
}
}
// MARK: Add goal sheet
private struct AddGoalSheet: View {
@Environment(\.modelContext) private var context
@Environment(\.dismiss) private var dismiss
@Query(sort: \TaskItem.sortOrder) private var allTasks: [TaskItem]
@Query(sort: \TagEntity.sortOrder) private var allTags: [TagEntity]
@State private var title = ""
@State private var selectedTaskID: UUID? = nil
@State private var selectedTagID: UUID? = nil
@State private var period: GoalPeriod = .daily
@State private var unit: GoalUnit = .seconds
@State private var targetMin: Double = 1800
@State private var hasMax = false
@State private var targetMaxValue: Double = 3600
private var activeTasks: [TaskItem] { allTasks.filter { !$0.isArchived } }
private var canSave: Bool { !title.trimmingCharacters(in: .whitespaces).isEmpty }
var body: some View {
NavigationStack {
Form {
Section(String(localized: "goal.form.section.title")) {
TextField(String(localized: "goal.form.title.placeholder"), text: $title)
}
Section(String(localized: "goal.form.section.scope")) {
Picker(String(localized: "goal.form.task"), selection: $selectedTaskID) {
Text(String(localized: "goal.form.none")).tag(Optional<UUID>.none)
ForEach(activeTasks) { task in
Label(task.name, systemImage: task.icon).tag(Optional(task.id))
}
}
Picker(String(localized: "goal.form.tag"), selection: $selectedTagID) {
Text(String(localized: "goal.form.none")).tag(Optional<UUID>.none)
ForEach(allTags) { tag in
Text(tag.name).tag(Optional(tag.id))
}
}
}
Section(String(localized: "goal.form.section.period")) {
Picker("", selection: $period) {
ForEach(GoalPeriod.allCases, id: \.self) { p in
Text(p.localizedName).tag(p)
}
}
.pickerStyle(.segmented)
.labelsHidden()
}
Section(String(localized: "goal.form.section.target")) {
Picker("", selection: $unit) {
Text(String(localized: "goal.unit.duration")).tag(GoalUnit.seconds)
Text(String(localized: "goal.unit.count")).tag(GoalUnit.count)
}
.pickerStyle(.segmented)
.labelsHidden()
.onChange(of: unit) { _, newUnit in
targetMin = newUnit == .seconds ? 1800 : 1
targetMaxValue = newUnit == .seconds ? 3600 : 5
}
minValueStepper
Toggle(String(localized: "goal.form.has_max"), isOn: $hasMax)
if hasMax {
maxValueStepper
}
}
Section {
Toggle(String(localized: "goal.active"), isOn: .constant(true))
}
}
.navigationTitle(String(localized: "goal.add.title"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(String(localized: "action.cancel")) { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "action.save")) { save() }
.disabled(!canSave)
.tint(Color.tfPrimary)
}
}
}
}
@ViewBuilder
private var minValueStepper: some View {
if unit == .seconds {
Stepper(
value: $targetMin,
in: 60...86400,
step: 300
) {
Text("\(String(localized: "goal.form.min")): \(targetMin.hhmmss)")
.font(.subheadline.monospacedDigit())
}
} else {
Stepper(
value: $targetMin,
in: 1...9999,
step: 1
) {
Text("\(String(localized: "goal.form.min")): \(Int(targetMin))")
.font(.subheadline)
}
}
}
@ViewBuilder
private var maxValueStepper: some View {
if unit == .seconds {
Stepper(
value: $targetMaxValue,
in: targetMin...86400 * 7,
step: 300
) {
Text("\(String(localized: "goal.form.max")): \(targetMaxValue.hhmmss)")
.font(.subheadline.monospacedDigit())
}
} else {
Stepper(
value: $targetMaxValue,
in: targetMin...9999,
step: 1
) {
Text("\(String(localized: "goal.form.max")): \(Int(targetMaxValue))")
.font(.subheadline)
}
}
}
private func save() {
let goal = GoalEntity(
title: title.trimmingCharacters(in: .whitespaces),
targetValue: targetMin,
targetMax: hasMax ? targetMaxValue : nil,
unit: unit,
period: period
)
goal.task = activeTasks.first { $0.id == selectedTaskID }
goal.tag = allTags.first { $0.id == selectedTagID }
context.insert(goal)
dismiss()
}
}
#Preview {
GoalsTabView()
.modelContainer(for: [GoalEntity.self, TaskItem.self, TagEntity.self], inMemory: true)
.environment(AppPreferences())
}

View File

@ -1,217 +0,0 @@
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())
}

View File

@ -1,62 +0,0 @@
import SwiftUI
struct SettingsTabView: View {
@Environment(AppPreferences.self) private var preferences
var body: some View {
@Bindable var prefs = preferences
NavigationStack {
Form {
Section(String(localized: "settings.section.appearance")) {
Picker(String(localized: "settings.theme"), selection: $prefs.themeMode) {
ForEach(ThemeMode.allCases) { mode in
Text(mode.localizedName).tag(mode)
}
}
}
Section(String(localized: "settings.section.language")) {
Picker(String(localized: "settings.language"), selection: $prefs.languageCode) {
Text("한국어").tag("ko")
Text("English").tag("en")
}
}
Section(String(localized: "settings.section.calendar")) {
Picker(String(localized: "settings.week_start"), selection: $prefs.weekStartDay) {
Text(String(localized: "weekday.sunday")).tag(1)
Text(String(localized: "weekday.monday")).tag(2)
Text(String(localized: "weekday.saturday")).tag(7)
}
Stepper(value: $prefs.dayStartHour, in: 0...23) {
HStack {
Text(String(localized: "settings.day_start"))
Spacer()
Text(String(format: "%02d:00", prefs.dayStartHour))
.foregroundStyle(Color.tfMuted)
}
}
}
Section {
NavigationLink {
// TODO: premium paywall in milestone 3+
Text(String(localized: "premium.coming_soon"))
.foregroundStyle(Color.tfMuted)
} label: {
Label(String(localized: "settings.premium"), systemImage: "star.fill")
.foregroundStyle(Color.tfAccent)
}
}
}
.scrollContentBackground(.hidden)
.background(Color.tfBackground)
.navigationTitle(String(localized: "tab.settings"))
}
}
}
#Preview {
SettingsTabView()
.environment(AppPreferences())
}

View File

@ -1,19 +0,0 @@
import SwiftUI
struct StatsTabView: View {
var body: some View {
NavigationStack {
EmptyStateView(
icon: "chart.bar",
message: String(localized: "stats.empty")
)
.background(Color.tfBackground)
.navigationTitle(String(localized: "tab.stats"))
}
}
}
#Preview {
StatsTabView()
.environment(AppPreferences())
}

View File

@ -1,211 +0,0 @@
import SwiftUI
import SwiftData
struct TagsTabView: View {
@Environment(\.modelContext) private var context
@Query(sort: \TagEntity.sortOrder) private var allTags: [TagEntity]
@State private var showAddSheet = false
@State private var editTarget: TagEntity?
private var superTags: [TagEntity] { allTags.filter { $0.parent == nil } }
var body: some View {
NavigationStack {
Group {
if superTags.isEmpty {
EmptyStateView(
icon: "tag",
message: String(localized: "tags.empty")
)
} else {
List {
ForEach(superTags) { tag in
TagRowView(tag: tag, onEdit: { editTarget = $0 })
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
context.delete(tag)
} label: {
Label("삭제", systemImage: "trash")
}
Button {
editTarget = tag
} label: {
Label("편집", systemImage: "pencil")
}
.tint(.blue)
}
}
}
.scrollContentBackground(.hidden)
}
}
.background(Color.tfBackground)
.navigationTitle(String(localized: "tab.tags"))
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button { showAddSheet = true } label: {
Image(systemName: "plus")
}
.tint(Color.tfPrimary)
}
}
.sheet(isPresented: $showAddSheet) {
TagFormView(superTags: superTags)
}
.sheet(item: $editTarget) { tag in
TagFormView(existing: tag, superTags: superTags)
}
}
}
}
// MARK: Row
private struct TagRowView: View {
@Environment(\.modelContext) private var context
let tag: TagEntity
let onEdit: (TagEntity) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Circle()
.fill(Color(hex: tag.colorHex))
.frame(width: 10, height: 10)
Text(tag.name)
.foregroundStyle(Color.tfOnBackground)
Spacer()
if !tag.subTags.isEmpty {
Text(String(format: String(localized: "tags.subtag_count"), tag.subTags.count))
.font(.caption)
.foregroundStyle(Color.tfMuted)
}
}
ForEach(tag.subTags) { sub in
HStack(spacing: 8) {
Spacer().frame(width: 14)
Circle()
.fill(Color(hex: sub.colorHex))
.frame(width: 8, height: 8)
Text(sub.name)
.font(.subheadline)
.foregroundStyle(Color.tfMuted)
Spacer()
}
.contentShape(Rectangle())
.contextMenu {
Button("편집") { onEdit(sub) }
Button("삭제", role: .destructive) { context.delete(sub) }
}
}
}
.padding(.vertical, 4)
}
}
// MARK: Form
struct TagFormView: View {
@Environment(\.modelContext) private var context
@Environment(\.dismiss) private var dismiss
var existing: TagEntity? = nil
var superTags: [TagEntity]
@State private var name = ""
@State private var selectedColor = "#52A878"
@State private var selectedParent: TagEntity? = nil
private let colorPresets = [
"#52A878", "#1E5C40", "#4CBF80",
"#C99728", "#E8B84B", "#D64545",
"#4A90D9", "#9B59B6", "#E67E22",
"#1ABC9C"
]
private var isEditing: Bool { existing != nil }
private var isSaveDisabled: Bool { name.trimmingCharacters(in: .whitespaces).isEmpty }
private var availableParents: [TagEntity] {
superTags.filter { $0.id != existing?.id }
}
var body: some View {
NavigationStack {
Form {
Section("이름") {
TextField("태그 이름", text: $name)
}
Section("색상") {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 12) {
ForEach(colorPresets, id: \.self) { hex in
Circle()
.fill(Color(hex: hex))
.frame(width: 36, height: 36)
.overlay {
if hex == selectedColor {
Image(systemName: "checkmark")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(.white)
}
}
.onTapGesture { selectedColor = hex }
}
}
.padding(.vertical, 4)
}
Section("상위 태그") {
Picker("상위 태그", selection: $selectedParent) {
Text("없음 (슈퍼 태그)").tag(nil as TagEntity?)
ForEach(availableParents) { tag in
Text(tag.name).tag(tag as TagEntity?)
}
}
.pickerStyle(.menu)
}
}
.navigationTitle(isEditing ? "태그 편집" : "태그 추가")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("취소") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("저장") { save() }
.disabled(isSaveDisabled)
}
}
.onAppear { loadExisting() }
}
}
private func loadExisting() {
guard let tag = existing else { return }
name = tag.name
selectedColor = tag.colorHex
selectedParent = tag.parent
}
private func save() {
let trimmed = name.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
if let tag = existing {
tag.name = trimmed
tag.colorHex = selectedColor
tag.parent = selectedParent
} else {
let newTag = TagEntity(name: trimmed, colorHex: selectedColor)
newTag.parent = selectedParent
context.insert(newTag)
}
dismiss()
}
}
#Preview {
TagsTabView()
.modelContainer(for: [TagEntity.self, TaskItem.self], inMemory: true)
.environment(AppPreferences())
}

View File

@ -1,260 +0,0 @@
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())
}

View File

@ -1,18 +0,0 @@
//
// Item.swift
// TallyFlow
//
// Created by on 6/26/26.
//
import Foundation
import SwiftData
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}

View File

@ -1,870 +0,0 @@
{
"sourceLanguage" : "ko",
"strings" : {
"" : {
},
"—" : {
"comment" : "A separator between the summary card and the task action section.",
"isCommentAutoGenerated" : true
},
"·" : {
"comment" : "A period used to separate two items.",
"isCommentAutoGenerated" : true
},
"(%@)" : {
"comment" : "A tag that shows the name of the parent tag in parentheses.",
"isCommentAutoGenerated" : true
},
"%@: %@" : {
"comment" : "A stepper that lets the user select a minimum value for a goal. The value is displayed in a formatted time string. The argument is the string “goal.form.min”.",
"isCommentAutoGenerated" : true,
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@: %2$@"
}
}
}
},
"%@: %lld" : {
"comment" : "A label displaying the minimum value for a goal. The argument is the string “goal.form.min”.",
"isCommentAutoGenerated" : true,
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@: %2$lld"
}
}
}
},
"action.cancel" : {
"comment" : "The label of a button that cancels an action.",
"isCommentAutoGenerated" : true
},
"action.save" : {
"comment" : "The label of a button that saves the goal.",
"isCommentAutoGenerated" : true
},
"English" : {
"comment" : "A language option in the settings.",
"isCommentAutoGenerated" : true
},
"goal.active" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Active"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "활성"
}
}
}
},
"goal.add.title" : {
"comment" : "The title of the add goal sheet.",
"isCommentAutoGenerated" : true
},
"goal.form.has_max" : {
"comment" : "A toggle that allows users to specify a maximum value for a goal.",
"isCommentAutoGenerated" : true
},
"goal.form.max" : {
"comment" : "A stepper that lets the user select a maximum value for a goal. The value is displayed in a formatted time string.",
"isCommentAutoGenerated" : true
},
"goal.form.min" : {
"comment" : "A label displaying the minimum value of a goal. The value is formatted as a time.",
"isCommentAutoGenerated" : true
},
"goal.form.none" : {
"comment" : "A placeholder for a picker that allows the user to select a task.",
"isCommentAutoGenerated" : true
},
"goal.form.section.period" : {
"comment" : "A section in the goal form where the user can select the period for the goal.",
"isCommentAutoGenerated" : true
},
"goal.form.section.scope" : {
"comment" : "A section in the goal form that allows the user to select a task and a tag.",
"isCommentAutoGenerated" : true
},
"goal.form.section.target" : {
"comment" : "A section in the goal form that allows the user to set the target value.",
"isCommentAutoGenerated" : true
},
"goal.form.section.title" : {
"comment" : "A section in the goal form.",
"isCommentAutoGenerated" : true
},
"goal.form.tag" : {
"comment" : "A label for the tag picker in the goal form.",
"isCommentAutoGenerated" : true
},
"goal.form.task" : {
"comment" : "A label for the task picker in the goal form.",
"isCommentAutoGenerated" : true
},
"goal.form.title.placeholder" : {
"comment" : "A placeholder text for the goal's title.",
"isCommentAutoGenerated" : true
},
"goal.inactive" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Inactive"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "비활성"
}
}
}
},
"goal.period.daily" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Daily"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "매일"
}
}
}
},
"goal.period.monthly" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Monthly"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "매월"
}
}
}
},
"goal.period.weekly" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Weekly"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "매주"
}
}
}
},
"goal.unit.count" : {
"comment" : "A unit of measurement for a goal.",
"isCommentAutoGenerated" : true
},
"goal.unit.duration" : {
"comment" : "A unit of time for a goal.",
"isCommentAutoGenerated" : true
},
"goals.empty" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No goals yet"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "목표가 없습니다"
}
}
}
},
"main.actions.log_count" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Log Count"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "횟수 기록"
}
}
}
},
"main.actions.start_timer" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Start Timer"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "타이머 시작"
}
}
}
},
"main.actions.title" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Quick Actions"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "빠른 실행"
}
}
}
},
"main.layout_edit.placeholder" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Edit Layout (coming soon)"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "레이아웃 편집 (준비 중)"
}
}
}
},
"main.period.month" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This Month"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "이번 달"
}
}
}
},
"main.period.today" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Today"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "오늘"
}
}
}
},
"main.period.week" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This Week"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "이번 주"
}
}
}
},
"premium.coming_soon" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Coming Soon"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "준비 중입니다"
}
}
}
},
"settings.day_start" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Day Start Time"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "하루 시작 시간"
}
}
}
},
"settings.language" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Language"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "언어"
}
}
}
},
"settings.premium" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Premium"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "프리미엄"
}
}
}
},
"settings.section.appearance" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Appearance"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "외관"
}
}
}
},
"settings.section.calendar" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Calendar"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "캘린더"
}
}
}
},
"settings.section.language" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Language"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "언어"
}
}
}
},
"settings.theme" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Theme"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "테마"
}
}
}
},
"settings.week_start" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Week Start Day"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "주 시작일"
}
}
}
},
"splash.tagline" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Track. Grow."
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "기록하고, 성장하세요"
}
}
}
},
"stats.empty" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No statistics yet"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "아직 통계가 없습니다"
}
}
}
},
"tab.goals" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Goals"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "목표"
}
}
}
},
"tab.main" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Main"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "메인"
}
}
}
},
"tab.settings" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Settings"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "설정"
}
}
}
},
"tab.stats" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Stats"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "통계"
}
}
}
},
"tab.tags" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tags"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "태그"
}
}
}
},
"tab.tasks" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tasks"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "할일"
}
}
}
},
"tags.empty" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No tags yet"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "태그가 없습니다"
}
}
}
},
"tags.subtag_count" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%d subtags"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "하위 태그 %d개"
}
}
}
},
"TallyFlow" : {
"comment" : "The name of the app.",
"isCommentAutoGenerated" : true
},
"task.type.count" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Count"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "횟수"
}
}
}
},
"task.type.counter" : {
"comment" : "A label for a task with a counter.",
"isCommentAutoGenerated" : true
},
"task.type.time" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Time"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "시간"
}
}
}
},
"task.type.timer" : {
"comment" : "A label for a timer task.",
"isCommentAutoGenerated" : true
},
"tasks.empty" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No tasks yet"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "작업이 없습니다"
}
}
}
},
"theme.dark" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dark"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "다크"
}
}
}
},
"theme.light" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Light"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "라이트"
}
}
}
},
"theme.system" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "System"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "시스템"
}
}
}
},
"weekday.monday" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Monday"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "월요일"
}
}
}
},
"weekday.saturday" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Saturday"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "토요일"
}
}
}
},
"weekday.sunday" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sunday"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "일요일"
}
}
}
},
"삭제" : {
"comment" : "A button that deletes a tag.",
"isCommentAutoGenerated" : true
},
"상위 태그" : {
"comment" : "A section for selecting a parent tag.",
"isCommentAutoGenerated" : true
},
"색상" : {
"comment" : "A section for selecting a color for a tag.",
"isCommentAutoGenerated" : true
},
"아이콘" : {
"comment" : "A section for selecting an icon for the task.",
"isCommentAutoGenerated" : true
},
"없음 (슈퍼 태그)" : {
"comment" : "A placeholder for a tag that has no parent.",
"isCommentAutoGenerated" : true
},
"유형" : {
"comment" : "A section for selecting the type of task.",
"isCommentAutoGenerated" : true
},
"이름" : {
"comment" : "A label for the tag's name.",
"isCommentAutoGenerated" : true
},
"작업" : {
"comment" : "A heading for a list of tasks.",
"isCommentAutoGenerated" : true
},
"작업 이름" : {
"comment" : "A label for the task name field.",
"isCommentAutoGenerated" : true
},
"작업 추가" : {
"comment" : "The title of the task creation screen.",
"isCommentAutoGenerated" : true
},
"작업 편집" : {
"comment" : "The title of the screen for editing a task.",
"isCommentAutoGenerated" : true
},
"저장" : {
"comment" : "A button that saves the current tag.",
"isCommentAutoGenerated" : true
},
"취소" : {
"comment" : "A button that cancels the current action.",
"isCommentAutoGenerated" : true
},
"카운터" : {
"comment" : "A label describing a task type",
"isCommentAutoGenerated" : true
},
"타이머" : {
"comment" : "A label for a task type that is a timer.",
"isCommentAutoGenerated" : true
},
"태그" : {
"comment" : "A section for selecting tags for a task.",
"isCommentAutoGenerated" : true
},
"태그 이름" : {
"comment" : "A label for the name of a tag.",
"isCommentAutoGenerated" : true
},
"태그 추가" : {
"comment" : "A title for a view that lets the user add a tag.",
"isCommentAutoGenerated" : true
},
"태그 편집" : {
"comment" : "The title of a screen for editing a tag.",
"isCommentAutoGenerated" : true
},
"편집" : {
"comment" : "A button that opens a sheet for editing a tag.",
"isCommentAutoGenerated" : true
},
"한국어" : {
"comment" : "Korean for \"한국어\".",
"isCommentAutoGenerated" : true
}
},
"version" : "1.1"
}

View File

@ -1,26 +0,0 @@
import Foundation
import Observation
@Observable
final class AppPreferences {
// Calendar.weekday constants: 1 = Sunday, 2 = Monday 7 = Saturday
var themeMode: ThemeMode = ThemeMode(rawValue: UserDefaults.standard.string(forKey: "pref.themeMode") ?? "system") ?? .system {
didSet { UserDefaults.standard.set(themeMode.rawValue, forKey: "pref.themeMode") }
}
var languageCode: String = UserDefaults.standard.string(forKey: "pref.languageCode") ?? "ko" {
didSet { UserDefaults.standard.set(languageCode, forKey: "pref.languageCode") }
}
var weekStartDay: Int = (UserDefaults.standard.object(forKey: "pref.weekStartDay") as? Int) ?? 2 {
didSet { UserDefaults.standard.set(weekStartDay, forKey: "pref.weekStartDay") }
}
var dayStartHour: Int = (UserDefaults.standard.object(forKey: "pref.dayStartHour") as? Int) ?? 0 {
didSet { UserDefaults.standard.set(dayStartHour, forKey: "pref.dayStartHour") }
}
var dayStartMinute: Int = (UserDefaults.standard.object(forKey: "pref.dayStartMinute") as? Int) ?? 0 {
didSet { UserDefaults.standard.set(dayStartMinute, forKey: "pref.dayStartMinute") }
}
}

View File

@ -1,37 +0,0 @@
//
// TallyFlowApp.swift
// TallyFlow
//
// Created by on 6/26/26.
//
import SwiftUI
import SwiftData
@main
struct TallyFlowApp: App {
private let preferences = AppPreferences()
var sharedModelContainer: ModelContainer = {
let schema = Schema([
TaskItem.self,
TagEntity.self,
GoalEntity.self,
TrackingRecord.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
.environment(preferences)
}
.modelContainer(sharedModelContainer)
}
}

View File

@ -1,43 +0,0 @@
import SwiftUI
import UIKit
// MARK: Semantic adaptive color tokens
extension Color {
// Brand Greens
static let tfPrimary = Color(light: Color(hex: "#1E5C40"), dark: Color(hex: "#4CBF80"))
static let tfSecondary = Color(light: Color(hex: "#52A878"), dark: Color(hex: "#2D7050"))
// Brand Accent Amber Gold
static let tfAccent = Color(light: Color(hex: "#C99728"), dark: Color(hex: "#E8B84B"))
// Surfaces
static let tfBackground = Color(light: Color(hex: "#F5FAF7"), dark: Color(hex: "#0C100D"))
static let tfSurface = Color(light: Color(hex: "#FFFFFF"), dark: Color(hex: "#141D16"))
// Text / Content
static let tfOnBackground = Color(light: Color(hex: "#112318"), dark: Color(hex: "#E8F0EB"))
static let tfMuted = Color(light: Color(hex: "#5E7A69"), dark: Color(hex: "#5A7B66"))
static let tfSeparator = Color(light: Color(hex: "#DCE8E1"), dark: Color(hex: "#1E2B22"))
}
// MARK: Color helpers
extension Color {
init(light: Color, dark: Color) {
self.init(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)
})
}
init(hex: String) {
let cleaned = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#"))
var value: UInt64 = 0
Scanner(string: cleaned).scanHexInt64(&value)
self.init(
red: Double((value >> 16) & 0xFF) / 255,
green: Double((value >> 8) & 0xFF) / 255,
blue: Double( value & 0xFF) / 255
)
}
}

View File

@ -1,39 +0,0 @@
import SwiftUI
// MARK: ThemeMode ColorScheme
extension ThemeMode {
var colorScheme: ColorScheme? {
switch self {
case .system: return nil
case .light: return .light
case .dark: return .dark
}
}
}
// MARK: Shared reusable views
struct EmptyStateView: View {
let icon: String
let message: String
var body: some View {
VStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 48))
.foregroundStyle(Color.tfMuted)
Text(message)
.font(.subheadline)
.foregroundStyle(Color.tfMuted)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
}
#Preview("EmptyStateView") {
EmptyStateView(icon: "tag", message: "태그가 없습니다")
.background(Color.tfBackground)
}

View File

@ -1,17 +0,0 @@
import Foundation
enum ThemeMode: String, CaseIterable, Identifiable {
case system
case light
case dark
var id: String { rawValue }
var localizedName: String {
switch self {
case .system: return String(localized: "theme.system")
case .light: return String(localized: "theme.light")
case .dark: return String(localized: "theme.dark")
}
}
}

View File

@ -1,333 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXFileReference section */
72C598532FED9ACF00D61864 /* TallyFlow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TallyFlow.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
72C598552FED9ACF00D61864 /* IOS */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = IOS;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
72C598502FED9ACF00D61864 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
72C5984A2FED9ACF00D61864 = {
isa = PBXGroup;
children = (
72C598552FED9ACF00D61864 /* IOS */,
72C598542FED9ACF00D61864 /* Products */,
);
sourceTree = "<group>";
};
72C598542FED9ACF00D61864 /* Products */ = {
isa = PBXGroup;
children = (
72C598532FED9ACF00D61864 /* TallyFlow.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
72C598522FED9ACF00D61864 /* TallyFlow */ = {
isa = PBXNativeTarget;
buildConfigurationList = 72C598602FED9AD100D61864 /* Build configuration list for PBXNativeTarget "TallyFlow" */;
buildPhases = (
72C5984F2FED9ACF00D61864 /* Sources */,
72C598502FED9ACF00D61864 /* Frameworks */,
72C598512FED9ACF00D61864 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
72C598552FED9ACF00D61864 /* IOS */,
);
name = TallyFlow;
packageProductDependencies = (
);
productName = TallyFlow;
productReference = 72C598532FED9ACF00D61864 /* TallyFlow.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
72C5984B2FED9ACF00D61864 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2650;
LastUpgradeCheck = 2650;
TargetAttributes = {
72C598522FED9ACF00D61864 = {
CreatedOnToolsVersion = 26.5;
};
};
};
buildConfigurationList = 72C5984E2FED9ACF00D61864 /* Build configuration list for PBXProject "TallyFlow" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 72C5984A2FED9ACF00D61864;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 72C598542FED9ACF00D61864 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
72C598522FED9ACF00D61864 /* TallyFlow */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
72C598512FED9ACF00D61864 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
72C5984F2FED9ACF00D61864 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
72C5985E2FED9AD100D61864 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
72C5985F2FED9AD100D61864 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
72C598612FED9AD100D61864 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.TallyFlow;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
72C598622FED9AD100D61864 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.TallyFlow;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
72C5984E2FED9ACF00D61864 /* Build configuration list for PBXProject "TallyFlow" */ = {
isa = XCConfigurationList;
buildConfigurations = (
72C5985E2FED9AD100D61864 /* Debug */,
72C5985F2FED9AD100D61864 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
72C598602FED9AD100D61864 /* Build configuration list for PBXNativeTarget "TallyFlow" */ = {
isa = XCConfigurationList;
buildConfigurations = (
72C598612FED9AD100D61864 /* Debug */,
72C598622FED9AD100D61864 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 72C5984B2FED9ACF00D61864 /* Project object */;
}

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>TallyFlow.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>