ynnckcmprnl
ynnckcmprnl

Reputation: 4352

SwiftUI drag & drop implementation triggers EXC_BAD_ACCESS exception

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 want 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.

enter image description here

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
        }
    }
}

Upvotes: 0

Views: 92

Answers (1)

ynnckcmprnl
ynnckcmprnl

Reputation: 4352

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
    }
}

Upvotes: 0

Related Questions