From 0754a7ff13a4f5ea6d95a5f58e26562a803af5d1 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 11 Feb 2026 22:54:51 +0100 Subject: [PATCH] feat: added list overview and detail view --- MindDump/ContentView.swift | 12 +++--- MindDump/MindDumpApp.swift | 3 ++ MindDump/Models/TodoItem.swift | 15 ++++++++ MindDump/Models/TodoList.swift | 15 ++++++++ MindDump/ViewModels/ListStore.swift | 44 ++++++++++++++++++++++ MindDump/Views/ListDetailView.swift | 52 ++++++++++++++++++++++++++ MindDump/Views/ListsOverviewView.swift | 50 +++++++++++++++++++++++++ MindDump/Views/TodoEditorView.swift | 52 ++++++++++++++++++++++++++ MindDump/Views/TodoRowView.swift | 27 +++++++++++++ 9 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 MindDump/Models/TodoItem.swift create mode 100644 MindDump/Models/TodoList.swift create mode 100644 MindDump/ViewModels/ListStore.swift create mode 100644 MindDump/Views/ListDetailView.swift create mode 100644 MindDump/Views/ListsOverviewView.swift create mode 100644 MindDump/Views/TodoEditorView.swift create mode 100644 MindDump/Views/TodoRowView.swift diff --git a/MindDump/ContentView.swift b/MindDump/ContentView.swift index b000a7e..b5b48bd 100644 --- a/MindDump/ContentView.swift +++ b/MindDump/ContentView.swift @@ -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()) } diff --git a/MindDump/MindDumpApp.swift b/MindDump/MindDumpApp.swift index 8b3be13..50d2ffe 100644 --- a/MindDump/MindDumpApp.swift +++ b/MindDump/MindDumpApp.swift @@ -2,9 +2,12 @@ import SwiftUI @main struct MindDumpApp: App { + @State private var store = ListStore() + var body: some Scene { WindowGroup { ContentView() + .environment(store) } } } diff --git a/MindDump/Models/TodoItem.swift b/MindDump/Models/TodoItem.swift new file mode 100644 index 0000000..df91469 --- /dev/null +++ b/MindDump/Models/TodoItem.swift @@ -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 + } +} diff --git a/MindDump/Models/TodoList.swift b/MindDump/Models/TodoList.swift new file mode 100644 index 0000000..a5eeb07 --- /dev/null +++ b/MindDump/Models/TodoList.swift @@ -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 + } +} diff --git a/MindDump/ViewModels/ListStore.swift b/MindDump/ViewModels/ListStore.swift new file mode 100644 index 0000000..09adf4d --- /dev/null +++ b/MindDump/ViewModels/ListStore.swift @@ -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() + } +} diff --git a/MindDump/Views/ListDetailView.swift b/MindDump/Views/ListDetailView.swift new file mode 100644 index 0000000..93191e9 --- /dev/null +++ b/MindDump/Views/ListDetailView.swift @@ -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) + } + } +} diff --git a/MindDump/Views/ListsOverviewView.swift b/MindDump/Views/ListsOverviewView.swift new file mode 100644 index 0000000..e9b0fde --- /dev/null +++ b/MindDump/Views/ListsOverviewView.swift @@ -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]) + } + } +} diff --git a/MindDump/Views/TodoEditorView.swift b/MindDump/Views/TodoEditorView.swift new file mode 100644 index 0000000..1cbf7e8 --- /dev/null +++ b/MindDump/Views/TodoEditorView.swift @@ -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() + } +} diff --git a/MindDump/Views/TodoRowView.swift b/MindDump/Views/TodoRowView.swift new file mode 100644 index 0000000..2bc7b41 --- /dev/null +++ b/MindDump/Views/TodoRowView.swift @@ -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) + } + } +}