From e7af3e6fc8d8ab23112c5a1928844dbe884ae0b5 Mon Sep 17 00:00:00 2001 From: Terry Yiu Date: Wed, 9 Sep 2026 23:59:51 +0300 Subject: [PATCH] Add native drag-and-drop reorder and right-click Remove on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS's Form isn't List-backed, so .onMove's drag-to-reorder (which already worked on iOS) had no effect there — the only way to reorder was the manual chevron up/down buttons. Those are replaced with real drag-and-drop via .draggable/.dropDestination, and a right-click context menu now offers Remove for any non-default currency, since macOS has no swipe-to-delete gesture either. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy --- iosApp/iosApp/ContentView.swift | 78 ++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index ad281db..50d2153 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -37,6 +37,20 @@ struct ContentView: View { : state.fiatRows } + #if os(macOS) + /// [codes] with [moving] relocated to sit right before [target] — how macOS's manual + /// drag-and-drop (see `.dropDestination` above) computes its new currency order, since + /// there's no List to hand the reordering off to natively. + private func reorderedCodes(_ codes: [String], moving: String, toBeBefore target: String) -> [String] { + guard moving != target, let fromIndex = codes.firstIndex(of: moving) else { return codes } + var reordered = codes + reordered.remove(at: fromIndex) + let insertIndex = reordered.firstIndex(of: target) ?? reordered.count + reordered.insert(moving, at: insertIndex) + return reordered + } + #endif + @ViewBuilder private func form(for state: IosConverterState) -> some View { Form { @@ -141,7 +155,7 @@ struct ContentView: View { let displayedRows = displayedRows(for: state) - ForEach(Array(displayedRows.enumerated()), id: \.element.code) { index, row in + ForEach(displayedRows, id: \.code) { row in amountRow( label: currencyFieldLabel(for: row.code), value: row.amount, @@ -150,19 +164,35 @@ struct ContentView: View { onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) }, isPriced: state.pricedCurrencyCodes.contains(row.code), sourceName: state.sourceName, - isManualSource: state.isManualSource, - onMoveUp: index > 0 ? { - var codes = displayedRows.map(\.code) - codes.move(fromOffsets: [index], toOffset: index - 1) - viewModel.onFiatCurrenciesReordered(codes) - } : nil, - onMoveDown: index < displayedRows.count - 1 ? { - var codes = displayedRows.map(\.code) - codes.move(fromOffsets: [index], toOffset: index + 2) - viewModel.onFiatCurrenciesReordered(codes) - } : nil + isManualSource: state.isManualSource ) .deleteDisabled(row.code == state.defaultCurrencyCode) + #if os(macOS) + // Form isn't List-backed on macOS, so drag-and-drop reordering has to be + // wired up manually rather than coming for free from .onMove below. + .draggable(row.code) + .dropDestination(for: String.self) { draggedCodes, _ in + guard let draggedCode = draggedCodes.first else { return false } + viewModel.onFiatCurrenciesReordered( + reorderedCodes(displayedRows.map(\.code), moving: draggedCode, toBeBefore: row.code) + ) + return true + } + .contextMenu { + if row.code != state.defaultCurrencyCode { + Button(role: .destructive) { + viewModel.onFiatCurrencyToggled(row.code) + } label: { + Label( + IosLocalizationKt.localizedString( + resource: MR.strings.shared.remove_currency_content_description + ), + systemImage: "trash" + ) + } + } + } + #endif } .onMove(perform: displayedRows.count > 1 ? { indices, newOffset in var codes = displayedRows.map(\.code) @@ -234,31 +264,9 @@ struct ContentView: View { onChange: @escaping (String) -> Void, isPriced: Bool = true, sourceName: String = "", - isManualSource: Bool = false, - onMoveUp: (() -> Void)? = nil, - onMoveDown: (() -> Void)? = nil + isManualSource: Bool = false ) -> some View { HStack { - #if os(macOS) - // macOS's Form isn't List-backed, so .onMove's drag-to-reorder has no effect here; - // these buttons are macOS's equivalent of iOS's Edit-mode drag handles. - if onMoveUp != nil || onMoveDown != nil { - VStack(spacing: 2) { - Button(action: { onMoveUp?() }) { - Image(systemName: "chevron.up") - .frame(width: 16, height: 10) - } - .disabled(onMoveUp == nil) - Button(action: { onMoveDown?() }) { - Image(systemName: "chevron.down") - .frame(width: 16, height: 10) - } - .disabled(onMoveDown == nil) - } - .font(.system(size: 10)) - .buttonStyle(.borderless) - } - #endif VStack(alignment: .leading) { Text(label) if !isPriced && !isManualSource {