Add native drag-and-drop reorder and right-click Remove on macOS
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
This commit is contained in:
@@ -37,6 +37,20 @@ struct ContentView: View {
|
|||||||
: state.fiatRows
|
: 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
|
@ViewBuilder
|
||||||
private func form(for state: IosConverterState) -> some View {
|
private func form(for state: IosConverterState) -> some View {
|
||||||
Form {
|
Form {
|
||||||
@@ -141,7 +155,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
let displayedRows = displayedRows(for: state)
|
let displayedRows = displayedRows(for: state)
|
||||||
|
|
||||||
ForEach(Array(displayedRows.enumerated()), id: \.element.code) { index, row in
|
ForEach(displayedRows, id: \.code) { row in
|
||||||
amountRow(
|
amountRow(
|
||||||
label: currencyFieldLabel(for: row.code),
|
label: currencyFieldLabel(for: row.code),
|
||||||
value: row.amount,
|
value: row.amount,
|
||||||
@@ -150,19 +164,35 @@ struct ContentView: View {
|
|||||||
onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) },
|
onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) },
|
||||||
isPriced: state.pricedCurrencyCodes.contains(row.code),
|
isPriced: state.pricedCurrencyCodes.contains(row.code),
|
||||||
sourceName: state.sourceName,
|
sourceName: state.sourceName,
|
||||||
isManualSource: state.isManualSource,
|
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
|
|
||||||
)
|
)
|
||||||
.deleteDisabled(row.code == state.defaultCurrencyCode)
|
.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
|
.onMove(perform: displayedRows.count > 1 ? { indices, newOffset in
|
||||||
var codes = displayedRows.map(\.code)
|
var codes = displayedRows.map(\.code)
|
||||||
@@ -234,31 +264,9 @@ struct ContentView: View {
|
|||||||
onChange: @escaping (String) -> Void,
|
onChange: @escaping (String) -> Void,
|
||||||
isPriced: Bool = true,
|
isPriced: Bool = true,
|
||||||
sourceName: String = "",
|
sourceName: String = "",
|
||||||
isManualSource: Bool = false,
|
isManualSource: Bool = false
|
||||||
onMoveUp: (() -> Void)? = nil,
|
|
||||||
onMoveDown: (() -> Void)? = nil
|
|
||||||
) -> some View {
|
) -> some View {
|
||||||
HStack {
|
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) {
|
VStack(alignment: .leading) {
|
||||||
Text(label)
|
Text(label)
|
||||||
if !isPriced && !isManualSource {
|
if !isPriced && !isManualSource {
|
||||||
|
|||||||
Reference in New Issue
Block a user