To reorder a list in your Mac app, pass SwiftUI’s onMove source and destination directly to move(fromOffsets:toOffset:). The destination marks a boundary in the original collection, so manually subtracting one for downward moves produces the wrong result.

This walkthrough builds a small native macOS list using synthetic labels, explains the destination arithmetic, and provides a test matrix for diagnosing misplaced rows. The scope is reordering items within your own app’s list.

What does the onMove destination mean?

Apple defines the two onMove(perform:) arguments as offsets relative to the dynamic view’s underlying collection. The first argument is an IndexSet of source offsets; the second is the destination offset. These positions belong to the collection used by that particular ForEach. Apple’s onMove(perform:) documentation.

For move(fromOffsets:toOffset:), Apple defines the destination as the offset before which the moved elements should be inserted, with count also permitted to mean the end. Think of it as a boundary between elements in the original collection. Apple’s move(fromOffsets:toOffset:) documentation.

Consider the original order A, B, C, D, E. Its element offsets are 0, 1, 2, 3, 4, while its insertion boundaries run from 0 through 5. Boundary 4 is immediately before E; boundary 5 is after the final item.

Moving B from source offset 1 to destination 4 therefore produces A, C, D, B, E. The moved item ends at index 3, even though the destination argument was 4. Both values use zero-based positions; they describe different stages of the operation.

That distinction explains the common downward-move error. Code that changes destination 4 to 3 before calling the collection’s move method requests a different boundary and places B before D. Forward the callback values unchanged when using Apple’s move method.

Build a complete reorderable macOS list

Use a native macOS SwiftUI app project in Xcode. The complete example below targets macOS 11 or later because it uses the SwiftUI App lifecycle; Apple lists onMove itself as available from macOS 10.15. Replace the generated app and content-view declarations with this code, leaving exactly one @main entry point in the target. Apple’s App documentation, Apple’s onMove(perform:) documentation.

import SwiftUI
import Foundation

struct DemoItem: Identifiable {
    let id: Int
    let label: String

    static let samples: [DemoItem] = [
        DemoItem(id: 0, label: "A"),
        DemoItem(id: 1, label: "B"),
        DemoItem(id: 2, label: "C"),
        DemoItem(id: 3, label: "D"),
        DemoItem(id: 4, label: "E")
    ]
}

@main
struct ReorderDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ReorderView()
        }
    }
}

struct ReorderView: View {
    @State private var items = DemoItem.samples
    @State private var selection = Set<Int>()
    @State private var lastMove = "No move yet"

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Drag rows to change their order")

            List(selection: $selection) {
                ForEach(items) { item in
                    Text(item.label)
                        .tag(item.id)
                }
                .onMove(perform: moveItems)
            }

            Text("Order: " + items.map(\.label).joined(separator: ", "))
            Text(lastMove)

            Button("Reset") {
                items = DemoItem.samples
                selection.removeAll()
                lastMove = "No move yet"
            }
        }
        .padding()
        .frame(minWidth: 420, minHeight: 300)
    }

    private func moveItems(from source: IndexSet, to destination: Int) {
        lastMove = "Source: \(Array(source)); destination: \(destination)"
        items.move(fromOffsets: source, toOffset: destination)
    }
}

The ForEach renders the same array that the handler mutates. Each item has a fixed identifier independent of its current position. Apple requires identifiable elements or an explicit identity key for ForEach; retaining each identifier during a move lets the view continue representing the same item. Apple’s ForEach documentation.

The array is local view state. Changing it causes SwiftUI to update dependent views, including the list and the displayed order. This example keeps the order in memory and contains no persistence implementation. Apple’s State documentation.

Run the app, drag B to the boundary immediately before E, and inspect both the order and callback display. Use Reset before trying another scenario. The explicit selection binding also supports selecting multiple rows; Apple documents selection sets and Mac selection without entering edit mode. Apple’s List documentation.

Do not add EditButton() to this native macOS example. Apple’s current availability information includes iOS and Mac Catalyst, but excludes native macOS. A tutorial’s mobile editing controls should not be copied into a Mac target without checking their availability. Apple’s EditButton documentation.

How can you calculate the final insertion index?

You only need this calculation when implementing removal and insertion yourself, or when translating the move into another model operation. It is an explanatory model derived from Apple’s documented insertion semantics, not an extra adjustment to apply before calling the built-in move method.

The calculation is:

insertion index after removal = original destination
                             - number of source offsets below destination

For source {1} and destination 4, one removed element originally precedes the boundary. The reduced array is A, C, D, E, and the insertion index is 4 - 1 = 3. Inserting B there produces A, C, D, B, E.

Multiple sources make a single downward correction inadequate. With sources {1, 3} and destination 5, the moved items are B, D; the remaining array is A, C, E. Two source offsets precede destination 5, so the insertion index is 3. The result is A, C, E, B, D.

A custom implementation must first capture the moved elements in their original order, then remove the originals without allowing earlier removals to invalidate later source positions. It can then calculate the adjusted insertion position and insert the captured block once. For this array-backed list, Apple’s existing method avoids implementing those steps yourself.

Which edge cases should you test?

The following cases are illustrative calculations for the original order A, B, C, D, E. Reset that order before each case. The last column is the expected model result, independent of how the drop indicator appears.

Source offsets Original destination Insertion index after removal Expected order
{1} 4 3 A, C, D, B, E
{3} 1 1 A, D, B, C, E
{1} 5 4 A, C, D, E, B
{1} 1 1 A, B, C, D, E
{1} 2 1 A, B, C, D, E
{1, 3} 5 3 A, C, E, B, D
{1, 3} 3 2 A, C, B, D, E

The two single-item no-op cases matter: inserting an item at its own boundary or the following boundary leaves its order unchanged. Apple documents this behavior explicitly. The end boundary is also valid even though it cannot be used to subscript an element. Apple’s move(fromOffsets:toOffset:) documentation.

The arithmetic model behind these seven cases was checked with a small deterministic Python calculation. The SwiftUI program was inspected but was not compiled or run here because a Swift toolchain and macOS runtime were unavailable. Verify the actual dragging interaction on your target macOS version.

Why can rows still move incorrectly?

Start with the collection boundary, then check identity and mutation. This sequence distinguishes destination arithmetic problems from mismatches between the displayed rows and stored data.

  1. Check the collection passed to ForEach. If it displays a filtered array, its offsets describe that filtered array. For example, visible rows B, D have offsets 0, 1; applying source 0 to the full array would move A. The consequence follows from Apple’s documented collection-relative callback contract. Apple’s onMove(perform:) documentation.
  2. Check whether rendering sorts the data again. If every redraw sorts by label, changing the backing array’s order cannot establish a lasting manual display order. Choose a manual ordering policy before enabling reordering in that view.
  3. Check identifiers. Preserve each item’s identifier while moving it. Do not regenerate identifiers during rendering or use changing positions as the identity of movable items. Apple’s ForEach documentation.
  4. Check for duplicate work. Remove manual destination subtraction, then confirm that the handler applies the move once to the authoritative collection. A second move acts on an already changed order.

For filtered or sectioned interfaces, define the intended behavior before translating indices. Decide whether a move rearranges only visible items, changes the complete collection, or transfers an item between sections. Map visible items to model identities and resolve the destination according to that policy. A numeric offset alone cannot express all those choices.

Keep callback offsets tied to the collection snapshot they describe. If the app defers the operation while other edits change the array, resolve the intended items and destination again rather than replaying stale positions. The sample avoids that complication by mutating its local array immediately.

Do AppKit or TextKit change the destination rules?

For an app already using NSTableView, AppKit has its own drop contract. NSTableView.DropOperation.above means insertion above a row, while .on means dropping on that row. Apple documents a drop below the last row as row n with operation .above, where n is the number of rows. Interpret both the row and operation before translating an AppKit drop into a model move. Apple’s NSTableView.DropOperation documentation.

TextKit addresses text storage, layout, and editing through components such as NSTextView and text layout managers. It does not supply the array-reordering contract used in this example. If a row also contains an editor, keep its text-editing implementation separate from the collection operation that changes row order. Apple’s TextKit documentation.

For a straightforward Mac list, keep one manually ordered collection, preserve item identifiers, and pass the onMove arguments directly to the collection’s move method. Use the boundary calculation and test matrix when you need to diagnose the result or translate the operation into a different model.

Sources