swiftuidrag-and-drop

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

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

Solution

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