0

I have created a drag-and-drop viewmodifier that works as expected, but now I would like to make it accept any object. I can add <T: Identifiable> to all the functions, structs, and view-modifiers, but when I try to do add it to my singleton class, I get "Static stored properties not supported in generic types".

I need the singleton class, so I can put the .dropObjectOutside viewmodifier anywhere in my view-hierarchy, so I've tried downcasting the ID to a String, but I can't seem to make that work.

Is there a way to downcast or make this code accept any object?

import SwiftUI

// I want this to be any object
struct StopContent: Identifiable {
    var id: String = UUID().uuidString
}

// Singleton class to hold drag state
class DragToReorderController: ObservableObject {

    // Make it a singleton, so it can be accessed from any view
    static let shared = DragToReorderController()
    private init() { }

    @Published var draggedID: String? // How do I make this a T.ID or downcast T.ID to string everywhere else?
    @Published var dragActive:Bool = false

}


// Add ViewModifier to view
extension View {

    func dragToReorder(_ item: StopContent, array: Binding<[StopContent]>) -> some View {
        self.modifier(DragToReorderObject(sourceItem: item, contentArray: array))
    }

    func dropOutside() -> some View {
        self.onDrop(of: [UTType.text], delegate: DropObjectOutsideDelegate())
    }
}


import UniformTypeIdentifiers

// MARK: View Modifier
struct DragToReorderObject: ViewModifier {

    let sourceItem: StopContent
    @Binding var contentArray: [StopContent]

    @ObservedObject private var dragReorder = DragToReorderController.shared

    func body(content: Content) -> some View {
        content
        .onDrag {
            dragReorder.draggedID = sourceItem.id
            dragReorder.dragActive = false
            return NSItemProvider(object: String(sourceItem.id) as NSString)
        }
        .onDrop(of: [UTType.text], delegate: DropObjectDelegate(sourceItem: sourceItem, listData: $contentArray, draggedItem: $dragReorder.draggedID, dragActive: $dragReorder.dragActive))
        .onChange(of: dragReorder.dragActive, perform: { value in
            if value == false {
                // Drag completed
            }
        })
        .opacity(dragReorder.draggedID == sourceItem.id && dragReorder.dragActive ? 0 : 1)
            
    }
}

// MARK: Drop and reorder
struct DropObjectDelegate: DropDelegate {

    let sourceItem: StopContent
    @Binding var listData: [StopContent]
    @Binding var draggedItem: String?
    @Binding var dragActive: Bool


    func dropEntered(info: DropInfo) {
    
        if draggedItem == nil { draggedItem = sourceItem.id }
    
        dragActive = true
    
        // Make sure the dragged item has moved and that it still exists
        if sourceItem.id != draggedItem {
            if let draggedItemValid = draggedItem {
                if let from = listData.firstIndex(where: { $0.id == draggedItemValid } ) {
                
                    // If that is true, move it to the new location
                    let to = listData.firstIndex(where: { $0.id == sourceItem.id } )!
                
                    if listData[to].id != draggedItem! {
                        listData.move(fromOffsets: IndexSet(integer: from),
                            toOffset: to > from ? to + 1 : to)
                    }
                }
            }
        }
    }

    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }

    func performDrop(info: DropInfo) -> Bool {
        dragActive = false
        draggedItem = nil
        return true
    }

}


// MARK: Drop and cancel
struct DropObjectOutsideDelegate: DropDelegate {

    // Using a singleton so we can drop anywhere
    @ObservedObject private var dragReorder = DragToReorderController.shared
    
    func dropEntered(info: DropInfo) {
        dragReorder.dragActive = true
    }

    func performDrop(info: DropInfo) -> Bool {
        dragReorder.dragActive = false
        dragReorder.draggedID = nil
        return true
    }
}
lmunck
  • 505
  • 4
  • 12

1 Answers1

1

For this, you have to add Identifiable generic constraint everywhere. Also, use Int for draggedID instead of String.

Here is the demo code.

// Singleton class to hold drag state
class DragToReorderController: ObservableObject {

    // Make it a singleton, so it can be accessed from any view
    static let shared = DragToReorderController()
    private init() { }

    @Published var draggedID: Int?
    @Published var dragActive: Bool = false
}


// Add ViewModifier to view
extension View {

    func dragToReorder<T: Identifiable>(_ item: T, array: Binding<[T]>) -> some View {
        self.modifier(DragToReorderObject(sourceItem: item, contentArray: array))
    }

    func dropOutside() -> some View {
        self.onDrop(of: [UTType.text], delegate: DropObjectOutsideDelegate())
    }
}


import UniformTypeIdentifiers

// MARK: View Modifier
struct DragToReorderObject<T: Identifiable>: ViewModifier {
    
    let sourceItem: T
    @Binding var contentArray: [T]
    
    @ObservedObject private var dragReorder = DragToReorderController.shared
    
    func body(content: Content) -> some View {
        content
            .onDrag {
                dragReorder.draggedID = sourceItem.id.hashValue
                dragReorder.dragActive = false
                return NSItemProvider(object: String(sourceItem.id.hashValue) as NSString)
            }
            .onDrop(of: [UTType.text], delegate: DropObjectDelegate(sourceItem: sourceItem, listData: $contentArray, draggedItem: $dragReorder.draggedID, dragActive: $dragReorder.dragActive))
            .onChange(of: dragReorder.dragActive, perform: { value in
                if value == false {
                    // Drag completed
                }
            })
            .opacity((dragReorder.draggedID == sourceItem.id.hashValue) && dragReorder.dragActive ? 0 : 1)
    }
}

// MARK: Drop and reorder
struct DropObjectDelegate<T: Identifiable>: DropDelegate {

    let sourceItem: T
    @Binding var listData: [T]
    @Binding var draggedItem: Int?
    @Binding var dragActive: Bool


    func dropEntered(info: DropInfo) {
    
        if draggedItem == nil { draggedItem = sourceItem.id.hashValue }
    
        dragActive = true
    
        // Make sure the dragged item has moved and that it still exists
        if sourceItem.id.hashValue != draggedItem {
            if let draggedItemValid = draggedItem {
                if let from = listData.firstIndex(where: { $0.id.hashValue == draggedItemValid } ) {
                
                    // If that is true, move it to the new location
                    let to = listData.firstIndex(where: { $0.id == sourceItem.id } )!
                
                    if listData[to].id.hashValue != draggedItem! {
                        listData.move(fromOffsets: IndexSet(integer: from),
                            toOffset: to > from ? to + 1 : to)
                    }
                }
            }
        }
    }

    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }

    func performDrop(info: DropInfo) -> Bool {
        dragActive = false
        draggedItem = nil
        return true
    }

}


// MARK: Drop and cancel
struct DropObjectOutsideDelegate: DropDelegate {

    // Using a singleton so we can drop anywhere
    @ObservedObject private var dragReorder = DragToReorderController.shared
    
    func dropEntered(info: DropInfo) {
        dragReorder.dragActive = true
    }

    func performDrop(info: DropInfo) -> Bool {
        dragReorder.dragActive = false
        dragReorder.draggedID = nil
        return true
    }
}
Raja Kishan
  • 16,767
  • 2
  • 26
  • 52
  • Ok, so if I read this right, you’re using Int instead of String so you can leverage that T.ID has a hashvalue which allows you to interact with the class. That was the point that had me stumped before, so this could be the answer I’m looking for. I’ll verify when it’s not 7am and in on my phone with the kids screaming in background. Thank you! – lmunck Jun 02 '21 at 05:12
  • 1
    try "\(sourceItem.id)" for your case insted of Int. – Raja Kishan Jun 02 '21 at 05:22
  • This works exactly as expected. Thank you so much. These techniques will save me a TON of redundant code in my apps, and I was never able to implement them in practical use-cases until you showed me how. – lmunck Jun 02 '21 at 08:03