Drag and drop a color from source to destination in SwiftUI on macOS

Viewed 626

How can I drag and drop a color from a source to a destination and have it stay? I currently have code working that does this on a temporary basis (changes color of destination on drag from source) but it quickly changes back on mouse up. So how can I have the color-change stay on mouse up over a specific cell and be able to change that cell again on future drags on mouse up over that specific cell? I am basing my current code off of here.

drawing

import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack {
            ColorSource(color: .purple, color2: .orange)
            Spacer()
            ColorSource(color: .blue, color2: .pink)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct DestinationDataKey: PreferenceKey {
    typealias Value = [DestinationData]

    static var defaultValue: [DestinationData] = []

    static func reduce(value: inout [DestinationData], nextValue: () -> [DestinationData]) {
        value.append(contentsOf: nextValue())
    }
}

struct DestinationData: Equatable {
    let destination: Int
    let frame: CGRect
}

struct DestinationDataSetter: View {
    let destination: Int

    var body: some View {
        GeometryReader { geometry in
            Rectangle()
                .fill(Color.clear)
                .preference(key: DestinationDataKey.self,
                            value: [DestinationData(destination: self.destination, frame: geometry.frame(in: .global))])
        }
    }
}

struct DestinationView: View {
    @Binding var active: Int
    let label: String
    let id: Int
    let color: Color

    var body: some View {
            Text(label).padding(10).background(self.active == id ? color : Color.green)
                .background(DestinationDataSetter(destination: id))
    }
}

struct ColorSource: View {
    @State var active = 0
    @State var destinations: [Int: CGRect] = [:]
    @State var color:Color
    @State var color2:Color
    @State var draggingFirstColor:Bool = false
    @State var draggingSecondColor:Bool = false

    var body: some View {
        VStack {

            Text("Drag From Here").padding().background(color)
                .gesture(DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
                    .onChanged { value in
                        self.draggingFirstColor = true
                        self.active = 0
                        for (id, frame) in self.destinations {
                            if frame.contains(value.location) {
                                self.active = id
                            }
                        }
                    }
                    .onEnded { value in
                        // do something on drop
                        self.active = 0
                        self.draggingFirstColor = false

                    }
            )

            Text("Drag From Here").padding().background(color2)
                .gesture(DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
                    .onChanged { value in
                        self.draggingSecondColor = true
                        self.active = 0
                        for (id, frame) in self.destinations {
                            if frame.contains(value.location) {
                                self.active = id
                            }
                        }
                    }
                    .onEnded { value in
                        // do something on drop
                        self.active = 0
                         self.draggingSecondColor = false
                    }
            )

            Divider()
            DestinationView(active: $active, label: "Drag Over Me", id: 1, color: (draggingFirstColor ? color : (draggingSecondColor ? color2 : Color.green)))
            DestinationView(active: $active, label: "Drag Over Me", id: 2, color: (draggingFirstColor ? color : (draggingSecondColor ? color2 : Color.green)))
            DestinationView(active: $active, label: "Drag Over Me", id: 3, color: (draggingFirstColor ? color : (draggingSecondColor ? color2 : Color.green)))
            DestinationView(active: $active, label: "Drag Over Me", id: 4, color: (draggingFirstColor ? color : (draggingSecondColor ? color2 : Color.green)))
        }.onPreferenceChange(DestinationDataKey.self) { preferences in
            for p in preferences {
                self.destinations[p.destination] = p.frame
            }
        }
    }
}
2 Answers

Just don't reset dragging color on end... or store it somewhere (say in model and use in destination as well)

demo

.onEnded { value in
    // do something on drop
    // self.active = 0
    // self.draggingFirstColor = false // same for second

}

Building on Asperi's response, I have implemented the ability to store the colors. I am using an enum with a variable to remember which color was last dragged, and a Color array to store and retrieve cell background colors based on last tapped color:

drawing

import SwiftUI

struct ContentView: View {
    @State var permanentColoring: [Color] = [.clear,.clear,.clear,.clear,.clear,.clear,.clear,.clear,.clear,.clear]
    @State var lastColorDragged:colorsDragged = .none
    var body: some View {
        HStack {
            ColorSource(color: .purple, color2: .orange, permanentColoring: self.$permanentColoring, lastColorDragged: self.lastColorDragged)
            Spacer()
            ColorSource(color: .blue, color2: .pink, permanentColoring: self.$permanentColoring, lastColorDragged: self.lastColorDragged)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct DestinationDataKey: PreferenceKey {
    typealias Value = [DestinationData]

    static var defaultValue: [DestinationData] = []

    static func reduce(value: inout [DestinationData], nextValue: () -> [DestinationData]) {
        value.append(contentsOf: nextValue())
    }
}

struct DestinationData: Equatable {
    let destination: Int
    let frame: CGRect
}

struct DestinationDataSetter: View {
    let destination: Int

    var body: some View {
        GeometryReader { geometry in
            Rectangle()
                .fill(Color.clear)
                .preference(key: DestinationDataKey.self,
                            value: [DestinationData(destination: self.destination, frame: geometry.frame(in: .global))])
        }
    }
}

struct DestinationView: View {
    @Binding var active: Int
    let label: String
    let id: Int
    let color: Color
    let color2: Color
    @Binding var permanentColoring:[Color]
    @Binding var lastColorDragged:colorsDragged

    var body: some View {
        Text(label).padding(10).background( ((self.active == id) ) ? (self.lastColorDragged == .first ? color : (self.lastColorDragged == .second ? color2 : .yellow)) : self.permanentColoring[self.id])
                .background(DestinationDataSetter(destination: id))
            .onTapGesture {
                if(self.lastColorDragged == .first) {
                    print("First color dragged: \(self.color) and not \(self.color2)")
                    self.permanentColoring[self.id] = self.color
                    print(self.permanentColoring[self.id])
                }
                else if(self.lastColorDragged == .second){
                    print("Second color dragged: \(self.color2) and not \(self.color)")
                    self.permanentColoring[self.id] = self.color2
                    print(self.permanentColoring[self.id])
                }
        }
    }
}

enum colorsDragged {
    case first, second, none
}

struct ColorSource: View {
    @State var active = 0
    @State var destinations: [Int: CGRect] = [:]
    @State var color:Color
    @State var color2:Color
    @State var draggingFirstColor:Bool = false
    @State var draggingSecondColor:Bool = false
    @Binding var permanentColoring:[Color]
    @State var lastColorDragged:colorsDragged

    var body: some View {
        VStack {

            Text("Drag From Here").padding().background(color)
                .gesture(DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
                    .onChanged { value in
                        self.draggingFirstColor = true
                        self.lastColorDragged = colorsDragged.first
                        self.active = 0
                        for (id, frame) in self.destinations {
                            if frame.contains(value.location) {
                                self.active = id
                            }
                        }
                    }
                    .onEnded { value in
                        // do something on drop
//                        self.active = 0
//                        self.draggingFirstColor = false

                    }
            )

            Text("Drag From Here").padding().background(color2)
                .gesture(DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
                    .onChanged { value in
                        self.draggingSecondColor = true
                        self.lastColorDragged = colorsDragged.second
                        self.active = 0
                        for (id, frame) in self.destinations {
                            if frame.contains(value.location) {
                                self.active = id
                            }
                        }
                    }
                    .onEnded { value in
                        // do something on drop
//                        self.active = 0
//                         self.draggingSecondColor = false
                    }
            )

            Divider()
            DestinationView(active: $active, label: "Drag Over Me", id: 1, color: self.color, color2: self.color2, permanentColoring: self.$permanentColoring, lastColorDragged: self.$lastColorDragged)
            DestinationView(active: $active, label: "Drag Over Me", id: 2, color: self.color, color2: self.color2, permanentColoring: self.$permanentColoring, lastColorDragged: self.$lastColorDragged)
            DestinationView(active: $active, label: "Drag Over Me", id: 3, color: self.color, color2: self.color2, permanentColoring: self.$permanentColoring, lastColorDragged: self.$lastColorDragged)
            DestinationView(active: $active, label: "Drag Over Me", id: 4, color: self.color, color2: self.color2, permanentColoring: self.$permanentColoring, lastColorDragged: self.$lastColorDragged)
        }.onPreferenceChange(DestinationDataKey.self) { preferences in
            for p in preferences {
                self.destinations[p.destination] = p.frame
            }
        }
    }
}
Related