add native examples
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CategoryPicker: View {
|
||||
@Binding var selectedCategory: String
|
||||
|
||||
private let columns = [
|
||||
GridItem(.adaptive(minimum: 90), spacing: 8)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Category")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
ForEach(todoCategories, id: \.self) { category in
|
||||
let isSelected = selectedCategory == category
|
||||
|
||||
Button {
|
||||
selectedCategory = category
|
||||
} label: {
|
||||
Text(category)
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(isSelected ? Color.accentColor : Color(.systemGray5))
|
||||
)
|
||||
.foregroundStyle(isSelected ? .white : .primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var viewModel = TodoViewModel()
|
||||
@State private var showingAddSheet = false
|
||||
@State private var editingTodo: Todo? = nil
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 0) {
|
||||
// Filter bar
|
||||
FilterBar(
|
||||
currentFilter: viewModel.currentFilter,
|
||||
countForFilter: { viewModel.count(for: $0) },
|
||||
onFilterSelected: { viewModel.setFilter($0) }
|
||||
)
|
||||
|
||||
Divider()
|
||||
|
||||
// Todo list or empty state
|
||||
if viewModel.filteredTodos.isEmpty {
|
||||
EmptyStateView()
|
||||
} else {
|
||||
List {
|
||||
ForEach(viewModel.filteredTodos) { todo in
|
||||
TodoItemView(
|
||||
todo: todo,
|
||||
onToggle: {
|
||||
viewModel.toggleTodo(id: todo.id)
|
||||
},
|
||||
onTap: {
|
||||
editingTodo = todo
|
||||
}
|
||||
)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowSeparator(.visible)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
let filtered = viewModel.filteredTodos
|
||||
for index in indexSet {
|
||||
let todo = filtered[index]
|
||||
viewModel.deleteTodo(id: todo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Todos \u{2013} SwiftUI")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
showingAddSheet = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddSheet) {
|
||||
TodoFormView(
|
||||
editingTodo: nil,
|
||||
onSave: { title, category, priority in
|
||||
viewModel.addTodo(title: title, category: category, priority: priority)
|
||||
showingAddSheet = false
|
||||
},
|
||||
onCancel: {
|
||||
showingAddSheet = false
|
||||
}
|
||||
)
|
||||
.presentationDetents([.medium, .large])
|
||||
}
|
||||
.sheet(item: $editingTodo) { todo in
|
||||
TodoFormView(
|
||||
editingTodo: todo,
|
||||
onSave: { title, category, priority in
|
||||
viewModel.updateTodo(id: todo.id, title: title, category: category, priority: priority)
|
||||
editingTodo = nil
|
||||
},
|
||||
onCancel: {
|
||||
editingTodo = nil
|
||||
}
|
||||
)
|
||||
.presentationDetents([.medium, .large])
|
||||
}
|
||||
}
|
||||
.tint(Color(red: 0x62/255.0, green: 0x00/255.0, blue: 0xEE/255.0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EmptyStateView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "checklist")
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(Color(.systemGray3))
|
||||
|
||||
Text("No todos yet!\nTap + to add one")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FilterBar: View {
|
||||
let currentFilter: TodoFilter
|
||||
let countForFilter: (TodoFilter) -> Int
|
||||
let onFilterSelected: (TodoFilter) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(TodoFilter.allCases) { filter in
|
||||
let isSelected = currentFilter == filter
|
||||
let count = countForFilter(filter)
|
||||
|
||||
Button {
|
||||
onFilterSelected(filter)
|
||||
} label: {
|
||||
Text("\(filter.displayName) (\(count))")
|
||||
.font(.subheadline)
|
||||
.fontWeight(isSelected ? .semibold : .regular)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(isSelected ? Color.accentColor : Color(.systemGray5))
|
||||
)
|
||||
.foregroundStyle(isSelected ? .white : .primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PriorityPicker: View {
|
||||
@Binding var selectedPriority: Priority
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Priority")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Priority.allCases) { priority in
|
||||
let isSelected = selectedPriority == priority
|
||||
|
||||
Button {
|
||||
selectedPriority = priority
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Circle()
|
||||
.fill(colorForPriority(priority))
|
||||
.frame(width: 10, height: 10)
|
||||
Text(priority.displayName)
|
||||
.font(.subheadline)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(isSelected ? colorForPriority(priority).opacity(0.15) : Color(.systemGray6))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(isSelected ? colorForPriority(priority) : Color.clear, lineWidth: 2)
|
||||
)
|
||||
.foregroundStyle(isSelected ? colorForPriority(priority) : .primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func colorForPriority(_ priority: Priority) -> Color {
|
||||
switch priority {
|
||||
case .low: return .green
|
||||
case .medium: return .orange
|
||||
case .high: return .red
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TodoFormView: View {
|
||||
let editingTodo: Todo?
|
||||
let onSave: (String, String, Priority) -> Void
|
||||
let onCancel: () -> Void
|
||||
|
||||
@State private var title: String = ""
|
||||
@State private var selectedCategory: String = "Personal"
|
||||
@State private var selectedPriority: Priority = .medium
|
||||
|
||||
private var isEditing: Bool {
|
||||
editingTodo != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
TextField("What needs to be done?", text: $title)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
}
|
||||
|
||||
Section {
|
||||
CategoryPicker(selectedCategory: $selectedCategory)
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
|
||||
|
||||
Section {
|
||||
PriorityPicker(selectedPriority: $selectedPriority)
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
|
||||
}
|
||||
.navigationTitle(isEditing ? "Edit Todo" : "Add New Todo")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(isEditing ? "Save" : "Add") {
|
||||
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
onSave(trimmed, selectedCategory, selectedPriority)
|
||||
}
|
||||
.fontWeight(.semibold)
|
||||
.disabled(title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if let todo = editingTodo {
|
||||
title = todo.title
|
||||
selectedCategory = todo.category
|
||||
selectedPriority = todo.priority
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TodoItemView: View {
|
||||
let todo: Todo
|
||||
let onToggle: () -> Void
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
HStack(spacing: 12) {
|
||||
// Checkbox toggle
|
||||
Button(action: onToggle) {
|
||||
Image(systemName: todo.completed ? "checkmark.circle.fill" : "circle")
|
||||
.font(.title2)
|
||||
.foregroundStyle(todo.completed ? Color.accentColor : Color(.systemGray3))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// Title and metadata
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(todo.title)
|
||||
.font(.body)
|
||||
.strikethrough(todo.completed)
|
||||
.foregroundStyle(todo.completed ? .secondary : .primary)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
// Category tag
|
||||
Text(todo.category)
|
||||
.font(.caption)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 3)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(Color(.systemGray5))
|
||||
)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Priority dot and label
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(priorityColor(todo.priority))
|
||||
.frame(width: 8, height: 8)
|
||||
Text(todo.priority.displayName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color(.systemGray3))
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 16)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func priorityColor(_ priority: Priority) -> Color {
|
||||
switch priority {
|
||||
case .low: return .green
|
||||
case .medium: return .orange
|
||||
case .high: return .red
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user