184 lines
6.2 KiB
Swift
184 lines
6.2 KiB
Swift
//
|
|
// ActionTabView.swift
|
|
// HaruDanim
|
|
//
|
|
// Lists all `Action`s and hosts a sheet for creating new ones.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct ActionTabView: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Query(sort: \Action.createdAt, order: .reverse) private var actions: [Action]
|
|
|
|
@State private var isAddSheetPresented = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
ForEach(actions) { action in
|
|
HStack(spacing: 12) {
|
|
Image(systemName: action.iconName)
|
|
.frame(width: 28)
|
|
.foregroundStyle(Color.brandPrimary)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(action.name)
|
|
if !action.tags.isEmpty {
|
|
Text(action.tags.map(\.name).joined(separator: ", "))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Text(action.type == .time ? "시간" : "횟수")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.onDelete(perform: deleteActions)
|
|
}
|
|
.navigationTitle("행동")
|
|
.overlay {
|
|
if actions.isEmpty {
|
|
ContentUnavailableView(
|
|
"행동이 없어요",
|
|
systemImage: "bolt",
|
|
description: Text("오른쪽 위 + 버튼으로 행동을 추가하세요.")
|
|
)
|
|
}
|
|
}
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button {
|
|
isAddSheetPresented = true
|
|
} label: {
|
|
Label("행동 추가", systemImage: "plus")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $isAddSheetPresented) {
|
|
AddActionSheet()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func deleteActions(at offsets: IndexSet) {
|
|
for index in offsets {
|
|
modelContext.delete(actions[index])
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Add Action Sheet
|
|
|
|
private struct AddActionSheet: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@Query(sort: \Tag.createdAt, order: .reverse) private var allTags: [Tag]
|
|
|
|
@State private var name = ""
|
|
@State private var iconName = "star.fill"
|
|
@State private var type: ActionType = .time
|
|
@State private var selectedTagIDs: Set<UUID> = []
|
|
|
|
private var trimmedName: String {
|
|
name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section("이름") {
|
|
TextField("행동 이름", text: $name)
|
|
}
|
|
|
|
Section("아이콘 (SF Symbol)") {
|
|
HStack {
|
|
Image(systemName: iconName.isEmpty ? "questionmark" : iconName)
|
|
.foregroundStyle(Color.brandPrimary)
|
|
TextField("star.fill", text: $iconName)
|
|
.autocorrectionDisabled()
|
|
.textInputAutocapitalization(.never)
|
|
}
|
|
}
|
|
|
|
Section("유형") {
|
|
Picker("유형", selection: $type) {
|
|
Text("시간").tag(ActionType.time)
|
|
Text("횟수").tag(ActionType.count)
|
|
}
|
|
.pickerStyle(.segmented)
|
|
}
|
|
|
|
Section("꼬리표") {
|
|
if allTags.isEmpty {
|
|
Text("먼저 꼬리표를 추가하세요.")
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(allTags) { tag in
|
|
Button {
|
|
toggle(tag)
|
|
} label: {
|
|
HStack(spacing: 12) {
|
|
Circle()
|
|
.fill(Color(hex: tag.colorHex))
|
|
.frame(width: 16, height: 16)
|
|
Text(tag.name)
|
|
.foregroundStyle(.primary)
|
|
Spacer()
|
|
if selectedTagIDs.contains(tag.id) {
|
|
Image(systemName: "checkmark")
|
|
.foregroundStyle(Color.brandPrimary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("새 행동")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("취소") { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("저장") { save() }
|
|
.disabled(trimmedName.isEmpty)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func toggle(_ tag: Tag) {
|
|
if selectedTagIDs.contains(tag.id) {
|
|
selectedTagIDs.remove(tag.id)
|
|
} else {
|
|
selectedTagIDs.insert(tag.id)
|
|
}
|
|
}
|
|
|
|
private func save() {
|
|
let chosenTags = allTags.filter { selectedTagIDs.contains($0.id) }
|
|
let trimmedIcon = iconName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let action = Action(
|
|
name: trimmedName,
|
|
type: type,
|
|
iconName: trimmedIcon.isEmpty ? "circle" : trimmedIcon,
|
|
tags: chosenTags
|
|
)
|
|
modelContext.insert(action)
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ActionTabView()
|
|
.modelContainer(for: [Action.self, Tag.self, TimeSession.self, CountEntry.self], inMemory: true)
|
|
}
|