Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI, Notification about the end, the cancellation of a drag operation

I have implemented drag and drop for one view. Now I want to change the state of the source view at the start of a drag operation and change the state back at completion or cancellation. The following example code should illustrate this.

import UniformTypeIdentifiers

struct DragableView: View {
    @State var isActiveDropTarget: Bool = false
    @State var isDragged: Bool = false

    var body: some View {
        Text("Hello world")
            .overlay(RoundedRectangle(cornerRadius: 8)
                        .fill(Color.accentColor.opacity(0.1))
                        .opacity(isActiveDropTarget ? 1.0 : 0.0))
            .opacity(isDragged ? 0.5 : 1)
        .onDrag {
            isDragged = true
            return NSItemProvider(object: "Test" as NSString)
        }
        .onDrop(of: [UTType.data], delegate: self)
    }
}

extension DragableView: DropDelegate
{
    func dropEntered(info: DropInfo)
    {
        isActiveDropTarget = true
    }
    
    func dropExited(info: DropInfo)
    {
        isActiveDropTarget = false
    }

    func performDrop(info: DropInfo) -> Bool
    {
        isActiveDropTarget = false
        /* Handle drop
         ...
         */
        return true
    }
}

The problem with the current implementation is that I do notice when a drag operation starts (.onDrag is called), but I don't know when the operation ends.

like image 853
D. Mika Avatar asked Oct 25 '25 09:10

D. Mika


1 Answers

This is a bit late, but I'm sharing my solution because there is no recent answer. It uses a concept similar to that proposed by @FrontFacingWindowCleaner, but does not use Delegates.

Key concepts to implement isDragActive

  • This project uses SwiftUI's' Transferrable drag and drop View modifiers .draggable() and .dropDestination()
  • It declares an isDragActive @State var to hold the current dragging state
  • It also declares an isDragActive environment varable to inject the current dragging state into all child Views
  • It uses the isTargeted: closure with all of the drop target .dropDestination() view modifiers to track dragging status for all expected drop targets
  • It also implements a .dropDestination() view modifier on an outer View in order to continue tracking the drag when none of the expected drop targets is being targeted

Tips

  • Use .onChange(of: isDragActive) to trigger desired actions when dragging begins or ends anywhere within the outer View.
  • This project uses it to change the FocusState of the List view so the background color of a selection will change during a drag.

A complete buildable demo project is at: https://github.com/Whiffer/SwiftUI_isDragActive

import SwiftUI
import UniformTypeIdentifiers

struct ContentView: View {
    @State private var selection = Set<Node>()
    @FocusState private var isListFocused: Bool
    @State private var isDragActive = false  // <<<<
    var body: some View {
        VStack {
            List(nodes, id: \.self , children: \.children, selection: self.$selection) { node in
                NodeView(node: node)
            }
            .focused($isListFocused)
            Text("isDragActive: \(isDragActive.description)")
        }
        .dropDestination(for: Node.self) { _, _ in
            return false
        } isTargeted: { isDragActive = $0 }   // <<<<
        .onChange(of: isDragActive) { _, newValue in
            if newValue == true {
                print("Drag started")
                isListFocused = false
            }
            if newValue == false {
                print("Drag ended")
                isListFocused = true
            }
        }
        .environment(\.isDragActive, $isDragActive)  // <<<<
    }
}

struct NodeView: View {
    var node: Node
    @State private var isTargeted = false
    @Environment(\.isDragActive) private var isDragActive  // <<<<
    var body: some View {
        Text(node.name)
            .listRowBackground(RoundedRectangle(cornerRadius: 5, style: .circular)
                .padding(.horizontal, 10)
                .foregroundColor(isTargeted ? Color(nsColor: .selectedContentBackgroundColor) : Color.clear)
            )
            .draggable(node)
            .dropDestination(for: Node.self) { droppedNodes, _ in
                for droppedNode in droppedNodes {
                    print("\(droppedNode.name) dropped on: \(node.name)")
                }
                return true
            } isTargeted: {
                isTargeted = $0
                isDragActive.wrappedValue = $0  // <<<<
            }
    }
}

struct DragActive: EnvironmentKey {  // <<<<
    static var defaultValue: Binding<Bool> = .constant(false)
}

extension EnvironmentValues {
    var isDragActive: Binding<Bool> {  // <<<<
        get { self[DragActive.self] }
        set { self[DragActive.self] = newValue }
    }
}

let nodes: [Node] = [
    .init(name: "Clothing", children: [
        .init(name: "Hoodies"), .init(name: "Jackets"), .init(name: "Joggers"), .init(name: "Jumpers"),
        .init(name: "Jeans", children: [.init(name: "Regular", children: [.init(name: "Size 34"), .init(name: "Size 32"), ] ), .init(name: "Slim") ] ), ] ),
    .init(name: "Shoes", children: [.init(name: "Boots"), .init(name: "Sandals"), .init(name: "Trainers"), ] ),
    .init(name: "Socks", children: [.init(name: "Crew"), .init(name: "Dress"), .init(name: "Athletic"), ] ),
]

struct Node: Identifiable, Hashable, Codable {
    var id = UUID()
    var name: String
    var children: [Node]? = nil
}

extension Node: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(contentType: .node)
    }
}

extension UTType {
    // Add a "public.data" Exported Type Identifier for this on the Info tab for the Target's Settings
    static var node: UTType { UTType(exportedAs: "com.experiment.node") }
}
like image 66
Chuck H Avatar answered Oct 27 '25 01:10

Chuck H