[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-swiftui-drag-to-reorder-grid-ios-27":3},{"content":4,"lastModified":5},"\u003Cp>I have a file in my project called \u003Ccode>GridReorder.swift\u003C\u002Fcode>. Two hundred lines. It exists because SwiftUI would only let you drag things around inside a \u003Ccode>List\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp>I wrote it three years ago for a board of photo tiles. It measured frames with \u003Ccode>GeometryReader\u003C\u002Fcode>, matched them back to indices, and animated a placeholder while you dragged. It worked about eighty percent of the time. Rotate the device and the cached frames were wrong. Turn VoiceOver on and reordering was impossible, because the whole thing depended on a drag gesture assistive tech never sends.\u003C\u002Fp>\n\n\u003Cp>As of iOS 27, that file has no reason to exist. The reordering APIs Apple showed at WWDC26 shipped with the OS on September 14, and I spent Monday afternoon deleting code.\u003C\u002Fp>\n\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1551650975-87deedd944c3?w=1200&amp;q=80\" alt=\"A phone held in one hand showing a card-based app interface, with design tools open on a monitor behind it\" loading=\"lazy\">\u003C\u002Fp>\n\n\u003Ch2>What we had before\u003C\u002Fh2>\n\n\u003Cp>Inside a \u003Ccode>List\u003C\u002Fcode>, \u003Ccode>onMove\u003C\u002Fcode> was fine. You get a set of offsets, you call \u003Ccode>move(fromOffsets:toOffset:)\u003C\u002Fcode> on your array, done.\u003C\u002Fp>\n\n\u003Cp>The moment your data needed to live in a grid, a stack, or a horizontally scrolling row of cards, you fell off a cliff. You had \u003Ccode>onDrag\u003C\u002Fcode> and \u003Ccode>onDrop\u003C\u002Fcode>, which are transfer APIs. \u003Ccode>dropDestination(for:action:)\u003C\u002Fcode> tells you something was dropped somewhere, and then you spend an afternoon figuring out where \"somewhere\" is relative to every other tile. Mine recorded the midpoint of each visible tile in a named coordinate space. It was not good code and I knew it.\u003C\u002Fp>\n\n\u003Ch2>reorderable() and reorderContainer\u003C\u002Fh2>\n\n\u003Cp>The new API splits the job in two. You mark the content that can be reordered, then you mark the container it can be reordered inside.\u003C\u002Fp>\n\n\u003Cp>Marking the content is one modifier on the \u003Ccode>ForEach\u003C\u002Fcode>, not on the cell inside it. Marking the container is one modifier on the list, stack, grid, or custom layout that encloses it.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">struct Photo: Identifiable, Hashable, Sendable {\n    let id: UUID\n    var caption: String\n}\n\nstruct PhotoBoard: View {\n    @State private var photos: [Photo] = []\n    private let columns = [GridItem(.adaptive(minimum: 140), spacing: 12)]\n\n    var body: some View {\n        ScrollView {\n            LazyVGrid(columns: columns, spacing: 12) {\n                ForEach(photos) { photo in\n                    PhotoTile(photo: photo)\n                }\n                .reorderable()\n            }\n            .reorderContainer(for: Photo.self) { difference in\n                photos.apply(difference)\n            }\n        }\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Your item type needs an identifier conforming to \u003Ccode>Hashable\u003C\u002Fcode> and \u003Ccode>Sendable\u003C\u002Fcode>. If your model is \u003Ccode>Identifiable\u003C\u002Fcode> with a \u003Ccode>UUID\u003C\u002Fcode>, you already meet that, which means the \u003Ccode>Sendable\u003C\u002Fcode> requirement is really just the \u003Ca href=\"\u002Fposts\u002Fswift-6-strict-concurrency-data-races\u002F\">Swift 6 strict concurrency rules\u003C\u002Fa> showing up on your model types again. There's no delegate to register and no coordinate space to name.\u003C\u002Fp>\n\n\u003Ch2>The move closure does not give you two indices\u003C\u002Fh2>\n\n\u003Cp>This is the part that confuses you for ten minutes if you're coming from \u003Ccode>onMove\u003C\u002Fcode>. You don't get a from-index and a to-index. You get a \u003Ccode>ReorderDifference\u003C\u002Fcode>, and it speaks in identifiers.\u003C\u002Fp>\n\n\u003Cp>\u003Ccode>difference.sources\u003C\u002Fcode> is an array of the IDs being moved. \u003Ccode>difference.destination.position\u003C\u002Fcode> is either \u003Ccode>.before(someID)\u003C\u002Fcode> or \u003Ccode>.end\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp>That design is deliberate, and once you see why, you stop wanting indices. A drag can take a second or two. During that window your array can change underneath you: a sync pulls in a new photo, a background task inserts a row. Had the system handed you index 4 and index 9, those numbers might mean something different by the time you apply them. IDs don't drift.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-swift\">extension Array where Element == Photo {\n    mutating func apply(\n        _ difference: ReorderDifference&lt;UUID, ReorderableSingleCollectionIdentifier&gt;\n    ) {\n        let moving = filter { difference.sources.contains($0.id) }\n        guard !moving.isEmpty else { return }\n        removeAll { difference.sources.contains($0.id) }\n\n        switch difference.destination.position {\n        case .before(let target):\n            let index = firstIndex { $0.id == target } ?? endIndex\n            insert(contentsOf: moving, at: index)\n        case .end:\n            append(contentsOf: moving)\n        }\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Remove first, then insert. If you insert before removing, every forward move lands one slot too far. I did that twice before I read my own diff properly.\u003C\u002Fp>\n\n\u003Cp>Those two cases are the whole enum, incidentally. \u003Ccode>before\u003C\u002Fcode> and \u003Ccode>end\u003C\u002Fcode>. You don't need an \u003Ccode>after\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Ch2>Things to know before you delete the old code\u003C\u002Fh2>\n\n\u003Cp>\u003Cstrong>Every one of these symbols is iOS 27 and up.\u003C\u002Fstrong> My deployment target is iOS 26, so the new path sits behind \u003Ccode>if #available(iOS 27, *)\u003C\u002Fcode> and the \u003Ccode>DropDelegate\u003C\u002Fcode> code stays for everyone else. Shipping two reorder implementations is a real cost, and I went through the same gate with \u003Ca href=\"\u002Fposts\u002Fswiftui-liquid-glass-pitfalls\u002F\">the Liquid Glass rebuild last year\u003C\u002Fa>. If your floor allows it, raise the floor.\u003C\u002Fp>\n\n\u003Cp>The container modifier also takes an \u003Ccode>isEnabled\u003C\u002Fcode> flag, which the docs suggest flipping off while you sync. I wired mine to the same binding that drives my save spinner. That killed a class of \"the tile snapped back\" bug reports I'd been ignoring for months.\u003C\u002Fp>\n\n\u003Cp>One limit worth planning around: \u003Ccode>reorderContainer\u003C\u002Fcode> only covers moves inside the container. Dragging a tile to another window or another app is a separate job, handled by \u003Ccode>dragContainer(for:in:_:)\u003C\u002Fcode> and \u003Ccode>dropDestination(for:isEnabled:action:)\u003C\u002Fcode>, with the destination maths coming from \u003Ccode>reorderDestination(for:in:)\u003C\u002Fcode> on the drop session. I haven't needed any of that, and if you don't either, you can ignore those three modifiers entirely.\u003C\u002Fp>\n\n\u003Cp>\u003Cstrong>Multiple collections have their own pair.\u003C\u002Fstrong> \u003Ccode>reorderable(collectionID:)\u003C\u002Fcode> plus \u003Ccode>reorderContainer(for:in:isEnabled:move:)\u003C\u002Fcode> is for moving items between sections, like photos between albums. Same idea, more plumbing.\u003C\u002Fp>\n\n\u003Cp>And watch the name. Half the posts I read while working this out call it \u003Ccode>reorderableContainer\u003C\u002Fcode>. It's \u003Ccode>reorderContainer\u003C\u002Fcode>. Trust autocomplete over the internet.\u003C\u002Fp>\n\n\u003Ch2>What actually got deleted\u003C\u002Fh2>\n\n\u003Cp>\u003Ccode>GridReorder.swift\u003C\u002Fcode> is gone, along with the geometry helpers that came with it. The part I care about is the accessibility win. With a framework modifier doing the work, reordering now functions under VoiceOver and Switch Control without a line of code from me. For three years my answer to \"can I reorder this with VoiceOver?\" was no, and there was a note in the backlog about it. The note is gone too.\u003C\u002Fp>\n\n\u003Cp>I kept the availability gate and the array extension above in \u003Ca href=\"\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa>, not because they're clever, but because I get the ordering wrong every time I rewrite that insert.\u003C\u002Fp>\n\n\u003Cp>SwiftUI still isn't finished. But this was the widest remaining gap between \"declarative\" and \"I need a workaround\", and it closed on Monday.\u003C\u002Fp>\n","2026-09-17",1789991458761]