From 101475facca33f5c76930450b45a54a8e466e35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beatrice=20Dellac=C3=A0?= Date: Tue, 27 Jan 2026 02:50:09 +0100 Subject: [PATCH] implement drag and drop --- MultiChrono/ContentView.swift | 95 +++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 16 deletions(-) diff --git a/MultiChrono/ContentView.swift b/MultiChrono/ContentView.swift index 1c28e11..1811165 100644 --- a/MultiChrono/ContentView.swift +++ b/MultiChrono/ContentView.swift @@ -7,6 +7,7 @@ import SwiftUI import Combine +import UniformTypeIdentifiers class ContentViewModel: ObservableObject { @Published var stopwatches: [Stopwatch] = [] @@ -35,6 +36,11 @@ class ContentViewModel: ObservableObject { } } + func moveStopwatch(fromOffsets source: IndexSet, toOffset destination: Int) { + stopwatches.move(fromOffsets: source, toOffset: destination) + save() + } + func save() { if let encoded = try? JSONEncoder().encode(stopwatches) { UserDefaults.standard.set(encoded, forKey: saveKey) @@ -55,28 +61,23 @@ struct ContentView: View { @State private var isShowingAddSheet = false @State private var isShowingSettings = false @State private var selectedStopwatch: Stopwatch? + @State private var draggingStopwatch: Stopwatch? @Environment(\.scenePhase) private var scenePhase var body: some View { NavigationStack { - List { - ForEach(viewModel.stopwatches) { stopwatch in - Button { - selectedStopwatch = stopwatch - } label: { - StopwatchRow(stopwatch: stopwatch, onDelete: { - viewModel.deleteStopwatch(id: stopwatch.id) - }) - } - .buttonStyle(.plain) // Preserves the row layout and interactions - } - .onDelete { indexSet in - for index in indexSet { - viewModel.deleteStopwatch(at: index) + ScrollView { + LazyVStack(spacing: 0) { + ForEach(viewModel.stopwatches) { stopwatch in + StopwatchListItem( + stopwatch: stopwatch, + viewModel: viewModel, + selectedStopwatch: $selectedStopwatch, + draggingStopwatch: $draggingStopwatch + ) } } } - .listStyle(.plain) .navigationTitle("MultiChrono") .toolbar { ToolbarItem(placement: .topBarLeading) { @@ -126,7 +127,6 @@ struct ContentView: View { } } } - .onChange(of: scenePhase) { newPhase in if newPhase == .background || newPhase == .inactive { viewModel.save() @@ -135,6 +135,69 @@ struct ContentView: View { } } +struct StopwatchListItem: View { + @ObservedObject var stopwatch: Stopwatch + @ObservedObject var viewModel: ContentViewModel + @Binding var selectedStopwatch: Stopwatch? + @Binding var draggingStopwatch: Stopwatch? + + var body: some View { + Button { + selectedStopwatch = stopwatch + } label: { + StopwatchRow(stopwatch: stopwatch, onDelete: { + withAnimation { + viewModel.deleteStopwatch(id: stopwatch.id) + } + }) + } + .buttonStyle(.plain) + .padding(.horizontal) + .padding(.vertical, 8) + .background(Color(UIColor.systemBackground)) + .contentShape(Rectangle()) + .onDrag { + self.draggingStopwatch = stopwatch + return NSItemProvider(object: stopwatch.id.uuidString as NSString) + } + .onDrop(of: [UTType.text], delegate: StopwatchDropDelegate(item: stopwatch, viewModel: viewModel, draggingItem: $draggingStopwatch)) + + Divider() + .padding(.leading) + } +} + +struct StopwatchDropDelegate: DropDelegate { + let item: Stopwatch + let viewModel: ContentViewModel + @Binding var draggingItem: Stopwatch? + + func dropEntered(info: DropInfo) { + guard let draggingItem = draggingItem else { return } + + if draggingItem.id != item.id { + if let from = viewModel.stopwatches.firstIndex(where: { $0.id == draggingItem.id }), + let to = viewModel.stopwatches.firstIndex(where: { $0.id == item.id }) { + + withAnimation { + viewModel.stopwatches.move(fromOffsets: IndexSet(integer: from), + toOffset: to > from ? to + 1 : to) + } + } + } + } + + func performDrop(info: DropInfo) -> Bool { + viewModel.save() + draggingItem = nil + return true + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + return DropProposal(operation: .move) + } +} + #Preview { ContentView() }