feat: added list overview and detail view

This commit is contained in:
2026-02-11 22:54:51 +01:00
parent 5a10e30319
commit 0754a7ff13
9 changed files with 264 additions and 6 deletions

View File

@@ -2,16 +2,16 @@ import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
NavigationStack {
ListsOverviewView()
.navigationDestination(for: UUID.self) { listID in
ListDetailView(listID: listID)
}
}
.padding()
}
}
#Preview {
ContentView()
.environment(ListStore())
}

View File

@@ -2,9 +2,12 @@ import SwiftUI
@main
struct MindDumpApp: App {
@State private var store = ListStore()
var body: some Scene {
WindowGroup {
ContentView()
.environment(store)
}
}
}

View File

@@ -0,0 +1,15 @@
import Foundation
struct TodoItem: Identifiable {
let id: UUID
var title: String
var isCompleted: Bool
let createdAt: Date
init(id: UUID = UUID(), title: String, isCompleted: Bool = false, createdAt: Date = Date()) {
self.id = id
self.title = title
self.isCompleted = isCompleted
self.createdAt = createdAt
}
}

View File

@@ -0,0 +1,15 @@
import Foundation
struct TodoList: Identifiable {
let id: UUID
var name: String
var items: [TodoItem]
let isInbox: Bool
init(id: UUID = UUID(), name: String, items: [TodoItem] = [], isInbox: Bool = false) {
self.id = id
self.name = name
self.items = items
self.isInbox = isInbox
}
}

View File

@@ -0,0 +1,44 @@
import Foundation
import Observation
@Observable
class ListStore {
var lists: [TodoList]
init() {
self.lists = [TodoList(name: "Inbox", isInbox: true)]
}
func addList(name: String) {
let list = TodoList(name: name)
lists.append(list)
}
func deleteList(_ list: TodoList) {
guard !list.isInbox else { return }
lists.removeAll { $0.id == list.id }
}
func addItem(to listID: UUID, title: String) {
guard let index = lists.firstIndex(where: { $0.id == listID }) else { return }
let item = TodoItem(title: title)
lists[index].items.append(item)
}
func deleteItem(_ itemID: UUID, from listID: UUID) {
guard let listIndex = lists.firstIndex(where: { $0.id == listID }) else { return }
lists[listIndex].items.removeAll { $0.id == itemID }
}
func updateItem(_ itemID: UUID, in listID: UUID, title: String) {
guard let listIndex = lists.firstIndex(where: { $0.id == listID }),
let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return }
lists[listIndex].items[itemIndex].title = title
}
func toggleItemCompleted(_ itemID: UUID, in listID: UUID) {
guard let listIndex = lists.firstIndex(where: { $0.id == listID }),
let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return }
lists[listIndex].items[itemIndex].isCompleted.toggle()
}
}

View File

@@ -0,0 +1,52 @@
import SwiftUI
struct ListDetailView: View {
@Environment(ListStore.self) private var store
let listID: UUID
@State private var editorItem: TodoItem?
@State private var showingEditor = false
private var todoList: TodoList? {
store.lists.first { $0.id == listID }
}
var body: some View {
Group {
if let todoList {
if todoList.items.isEmpty {
ContentUnavailableView("Keine Einträge", systemImage: "tray")
} else {
List {
ForEach(todoList.items) { item in
TodoRowView(item: item, onToggle: {
store.toggleItemCompleted(item.id, in: listID)
}, onTap: {
editorItem = item
showingEditor = true
})
}
.onDelete { offsets in
for index in offsets {
store.deleteItem(todoList.items[index].id, from: listID)
}
}
}
}
}
}
.navigationTitle(todoList?.name ?? "")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
editorItem = nil
showingEditor = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingEditor) {
TodoEditorView(listID: listID, item: editorItem)
}
}
}

View File

@@ -0,0 +1,50 @@
import SwiftUI
struct ListsOverviewView: View {
@Environment(ListStore.self) private var store
@State private var showingAddList = false
@State private var newListName = ""
var body: some View {
List {
ForEach(store.lists) { list in
NavigationLink(value: list.id) {
HStack {
Text(list.name)
Spacer()
Text("\(list.items.count)")
.foregroundStyle(.secondary)
}
}
}
.onDelete(perform: deleteLists)
}
.navigationTitle("MindDump")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: { showingAddList = true }) {
Image(systemName: "plus")
}
}
}
.alert("Neue Liste", isPresented: $showingAddList) {
TextField("Name", text: $newListName)
Button("Abbrechen", role: .cancel) {
newListName = ""
}
Button("Erstellen") {
let name = newListName.trimmingCharacters(in: .whitespaces)
if !name.isEmpty {
store.addList(name: name)
}
newListName = ""
}
}
}
private func deleteLists(at offsets: IndexSet) {
for index in offsets {
store.deleteList(store.lists[index])
}
}
}

View File

@@ -0,0 +1,52 @@
import SwiftUI
struct TodoEditorView: View {
@Environment(ListStore.self) private var store
@Environment(\.dismiss) private var dismiss
let listID: UUID
var item: TodoItem?
@State private var title: String = ""
@FocusState private var titleFocused: Bool
private var isEditing: Bool { item != nil }
var body: some View {
NavigationStack {
Form {
TextField("Titel", text: $title)
.focused($titleFocused)
}
.navigationTitle(isEditing ? "Bearbeiten" : "Neuer Eintrag")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Abbrechen") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Fertig") { save() }
.disabled(title.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
.onAppear {
if let item {
title = item.title
}
titleFocused = true
}
}
}
private func save() {
let trimmed = title.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
if let item {
store.updateItem(item.id, in: listID, title: trimmed)
} else {
store.addItem(to: listID, title: trimmed)
}
dismiss()
}
}

View File

@@ -0,0 +1,27 @@
import SwiftUI
struct TodoRowView: View {
let item: TodoItem
let onToggle: () -> Void
var onTap: (() -> Void)?
var body: some View {
HStack {
Button(action: onToggle) {
Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(item.isCompleted ? .green : .secondary)
.imageScale(.large)
}
.buttonStyle(.borderless)
Button(action: { onTap?() }) {
Text(item.title)
.strikethrough(item.isCompleted)
.foregroundStyle(item.isCompleted ? .secondary : .primary)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
}