mycode/IOS/Views/CategoryView.swift
2026-06-18 22:16:28 +09:00

178 lines
6.1 KiB
Swift

import SwiftUI
import SwiftData
struct CategoryView: View {
@Environment(\.modelContext) private var modelContext
@State private var viewModel = CategoryViewModel()
@State private var showAddSheet = false
var body: some View {
ZStack {
Theme.background.ignoresSafeArea()
Group {
if viewModel.categories.isEmpty {
emptyState
} else {
categoryList
}
}
}
.navigationTitle(String(localized: "tab.category"))
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button { showAddSheet = true } label: {
Image(systemName: "plus")
.foregroundStyle(Theme.green)
}
}
}
.sheet(isPresented: $showAddSheet) {
AddCategorySheet { name, colorHex in
viewModel.addCategory(name: name, colorHex: colorHex)
}
}
.onAppear { viewModel.setup(context: modelContext) }
}
private var emptyState: some View {
VStack(spacing: 16) {
Image(systemName: "folder.badge.plus")
.font(.system(size: 56))
.foregroundStyle(Theme.green.opacity(0.6))
Text(String(localized: "category.empty"))
.foregroundStyle(Theme.primaryText.opacity(0.6))
}
}
private var categoryList: some View {
List {
ForEach(viewModel.categories) { category in
CategoryRow(category: category, taskCount: viewModel.taskCount(for: category))
.listRowBackground(Theme.surface)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
viewModel.deleteCategory(category)
} label: {
Label(String(localized: "category.delete"), systemImage: "trash")
}
}
}
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
}
}
// MARK: - Category Row
private struct CategoryRow: View {
let category: Category
let taskCount: Int
var body: some View {
HStack(spacing: 12) {
Circle()
.fill(Color(hex: category.colorHex))
.frame(width: 14, height: 14)
Text(category.name)
.font(.body)
.fontWeight(.medium)
.foregroundStyle(Theme.primaryText)
Spacer()
Text("\(taskCount) \(String(localized: "category.task.count"))")
.font(.caption)
.foregroundStyle(Theme.primaryText.opacity(0.5))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Theme.green.opacity(0.12), in: Capsule())
}
.padding(.vertical, 6)
}
}
// MARK: - Add Category Sheet
private struct AddCategorySheet: View {
@Environment(\.dismiss) private var dismiss
@State private var name = ""
@State private var selectedColor = CategoryViewModel.presetColors[0]
let onAdd: (String, String) -> Void
var body: some View {
NavigationStack {
Form {
Section {
TextField(String(localized: "category.name"), text: $name)
}
Section(String(localized: "category.color")) {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) {
ForEach(CategoryViewModel.presetColors, id: \.self) { colorHex in
Circle()
.fill(Color(hex: colorHex))
.frame(width: 42, height: 42)
.overlay(alignment: .center) {
if selectedColor == colorHex {
Image(systemName: "checkmark")
.font(.caption)
.fontWeight(.bold)
.foregroundStyle(.white)
}
}
.overlay(
Circle()
.strokeBorder(
selectedColor == colorHex ? Theme.primaryText.opacity(0.4) : Color.clear,
lineWidth: 2
)
)
.onTapGesture { selectedColor = colorHex }
}
}
.padding(.vertical, 8)
}
}
.navigationTitle(String(localized: "category.add"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(String(localized: "button.cancel")) { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "button.add")) {
let trimmed = name.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
onAdd(trimmed, selectedColor)
dismiss()
}
.disabled(name.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
}
}
}
// MARK: - Color Hex Extension
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let r = Double((int >> 16) & 0xFF) / 255
let g = Double((int >> 8) & 0xFF) / 255
let b = Double(int & 0xFF) / 255
self.init(red: r, green: g, blue: b)
}
}
#Preview {
NavigationStack {
CategoryView()
}
}