I've been struggling with a generic EXC_BAD_ACCESS
exception while implementing drag & drop in SwiftUI, even after dumbing it all the way down to the bare minimum it's still happening and I can't get my head around why.
In the example below I wan't to drag the blue rectangle and move it behind the second orange box. It works as long as I drop it on the second orange box, but once I hover over the orange box and exit the box with the mouse it crashes once releasing the dragged blue rectangle. Even though the blue rectangle moved/animated to the correct position.
Anybody who has got a hunch or knows why this would trigger the exception? It has something to do with dropping the blue rectangle outside a view tagged with the onDrop
view modifier, but I'm missing something.
import SwiftUI
struct MyView: View {
@State var index = 0
var body: some View {
HStack() {
Color.orange
.frame(width: 200, height: 100)
.onDrop(of: [.text], delegate: MinimalDropDelegate(destinationIndex: 0, index: $index))
if index == 0 {
Color.cyan
.frame(width: 25, height: 100)
.onDrag {
NSItemProvider()
}
}
Color.orange
.frame(width: 200, height: 100)
.onDrop(of: [.text], delegate: MinimalDropDelegate(destinationIndex: 1, index: $index))
if index == 1 {
Color.cyan
.frame(width: 25, height: 100)
.onDrag {
NSItemProvider()
}
}
}
}
}
struct MinimalDropDelegate: DropDelegate {
let destinationIndex: Int
@Binding var index: Int
func dropUpdated(info: DropInfo) -> DropProposal? {
.init(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
true
}
func dropEntered(info: DropInfo) {
withAnimation {
index = destinationIndex
}
}
}
The issue seems to be the removal/addition of the blue divider, it must cause some kind of internal inconsistency in the SwiftUI handling behind the curtains.
When I remove the index property and store everything in an array with a ForEach
it doesn't crash.
No need to remove the Binding
or add a argument to the NSItemProvider
init.
This showcases the working implementation:
private let cardWidth = 200.0
enum Item: Equatable {
case box(String)
case separator
}
extension Item: Identifiable {
var id: String {
switch self {
case .box(let id): id
case .separator: "|"
}
}
}
struct MyView: View {
@State var items: [Item] = [.box("0"), .separator, .box("1")]
var body: some View {
HStack() {
ForEach(items) { item in
Group {
switch item {
case .box(let id):
Color.orange
.frame(width: cardWidth)
.overlay {
Text(id)
.font(.system(size: 50))
.foregroundStyle(.white)
}
.onDrop(of: [.text], delegate: OffssetIterationDropDelegate(destination: item,
items: $items))
case .separator:
Color.cyan
.frame(width: 25)
.onDrag {
NSItemProvider()
}
}
}
.frame(height: 100)
}
}
}
}
struct OffssetIterationDropDelegate: DropDelegate {
let destination: Item
@Binding var items: [Item]
func dropUpdated(info: DropInfo) -> DropProposal? {
let moveBefore = info.location.x < cardWidth / 2
let fromIndex = items.firstIndex(of: .separator)
if let fromIndex {
let toIndex = items.firstIndex(of: destination)
if let toIndex {
let newIndex = toIndex + (moveBefore ? 0 : 1)
withAnimation {
items.move(fromOffsets: IndexSet(integer: fromIndex),
toOffset: newIndex)
}
}
}
return .init(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
true
}
}