Drag outside screen issue during drag and drop - swiftui

I write some code to drag a photo from external app to a rectangle area in my app. I want to achieve an effect that the rectangle changes to semitransparent when the photo is dragged inside the rectangle without release. And then the rectangle changes back to opacity when the photo is dragged outside of the rect area.
I put my app and Photos side by side where my app is on the left side. If I drag the photo in and out from right side, it works good. However, the dropExited method is never called, if I drag the photo out from the left side. The result is the rect stays in semitransparent.
It seems like the drag gesture is considered to cancel automatically INSTEAD of drop exits the area because of touching the screen border. What should I do to reset the rect to opacity in this case?
import SwiftUI
struct ContentView: View {
#State private var dragIn = false
var body: some View {
VStack {
Rectangle()
.foregroundColor(.gray)
.opacity(dragIn ? 0.5 : 1)
}
.onDrop(of: [.image], delegate: DropController(dragIn: $dragIn))
}
}
class DropController: DropDelegate{
#Binding var dragIn: Bool
init(dragIn: Binding<Bool>) {
_dragIn = dragIn
}
func dropExited(info: DropInfo) {
dragIn = false
print("out")
}
func dropUpdated(info: DropInfo) -> DropProposal? {
dragIn = true
print("dragging")
return nil
}
func performDrop(info: DropInfo) -> Bool {
true
}
}

Related

SwiftUI: On every Button click create a View and dismiss/delete it after a few seconds

I want to create an effect if you press on a button a view should be visible above the button and fade away. I have already this effect:
struct AnimationView: View {
#Binding var animate: Bool
var body: some View {
Text("-1")
.bold()
.opacity(animate ? 0 : 1)
.offset(y: animate ? -50 : 0)
}
}
My button which triggers $animate
#State var animate = false
Button {
withAnimation(.linear(duration: 1)) {
animate = true
}
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
animate = false
}
} label: {
'SomeView'
}
But my problem is that I only can have one of this View. I want to kinda spam the button which creates Views as much I pressed the button, which all fades away. Is this possible?
You can change the #State property to this #State var animate: [Your data type or Void if you don't need one] = []. On each tap, you can append a () or any data type that you want and construct the views with that array, and then remove them with your timers.

SwiftUI Canvas and Gestures

I have a canvas in which I'm drawing views. I want to be able to reposition each view on drag in the canvas, but any gesture inside an ItemView is not recognised, I was thinking of implementing gestures outside the canvas, but I couldn't find anything about canvas hit-testing to select the item I want to reposition using context.translateBy, I'm not completely sure how to proceed since I couldn't find much about using gestures inside the canvas with multiple views.
Current Code:
struct TestCanvasView: View {
#FetchRequest(fetchRequest: Item.all) var items
var currentItems: [Item] {
items.filter { $0.parent == nil }
}
#State var position = CGPoint.zero
var body: some View {
Canvas { context, size in
for currentItem in currentItems {
let itemView = context.resolveSymbol(id: currentItem.objectID)!
context.draw(itemView, at: currentItem.itemPosition)
context.translateBy(x: position.x, y: position.y)
}
} symbols: {
ForEach(currentItems) { item in
ItemView(item: item)
.tag(item.objectID)
// .gesture() doesn't work here
}
}
.gesture(DragGesture()
.onChanged { value in
position = value.location
})
}
}

SwiftUI: how to implement drawable grid?

I'm trying to port my Pathfinding Visualizer web app from JS to SwiftUI on MacOS. Web App hosted on GitHub
Currently stuck on implementing the grid's input.
I want to be able to draw walls on the grid by dragging the cursor.
Full Code On GitHub
Here is my current method:
struct NodeView: View {
#State var nodeInfo: Node
#Binding var mouseDown: Bool
var body: some View {
Rectangle()
.fill(nodeColor(state: nodeInfo.getState()))
.frame(width: 25, height: 25)
.onHover { hover in
if mouseDown && hover {
print("mouse hover")
nodeInfo.toggleWall()
}
if hover {
print("node: \(nodeInfo.id) hovered!")
}
}
.pressAction {
if mouseDown == false {
mouseDown = true
print("mouse down")
nodeInfo.toggleWall()
}
} onRelease: {
mouseDown = false
print("mouse up")
}
}
The code for pressAction
struct PressActions: ViewModifier {
var onPress: () -> Void
var onRelease: () -> Void
func body(content: Content) -> some View {
content
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
onPress()
})
.onEnded({ _ in
onRelease()
})
)
}
}
extension View {
func pressAction(onPress: #escaping (() -> Void), onRelease: #escaping (() -> Void)) -> some View {
modifier(PressActions(onPress: {
onPress()
}, onRelease: {
onRelease()
}))
}
}
The mindset is when a node detects that the mouse is pressed it will update the mouseDown variable passed down from the grid.
The other nodes on the grid will respond to the cursor hovering over them when mouseDown is true.
However this approach doesn't work as I wished. The nodes won't respond to the cursor when dragged across them most of the time. And even if it does, the response is quite laggy.
Is it because I'm using dragGesture to listen to mouse down and mouse up during the whole dragging action?
Currently trying to use NSEvent instead of gestures.
Update:
I've changed the implementation to using stateGesture on the content view instead of each node view.
But the time it takes from input to render seems proportional to the grid's size.
Also it seems the view won't update while the cursor is moving.
I've tried the implementation suggested by the post SwiftUI drag gesture across multiple subviews
This approach works. However, the input lag is proportional to the grid's size.
After profiling and searching for the cause of such behavior.
My theory is there's simply too much subviews inside contentView, causing SwiftUI to spend a considerable amount of time on diffing when the state changes.(If I'm wrong you're welcome to correct me)
I'm not sure if there is a way to solve this inside SwiftUI.
Eventually I choose to implement the grid view using UIKit's UIView.
With this approach I successfully implemented a grid that can draw walls with little to none latency.
If your interested with the code, the code is posted at GitHub

SwiftUI: drag drop cell out of view not resetting overlay?

I am using this solution from Apseri:
SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?
with this bug fix:
SwiftUI: `onDrop` overlay not going away in LazyVGrid?
Just for some reference here, the grid snippet looks like this:
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var isUpdating = false
#State private var dragging: GridData?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
GridItemView(d: d)
.overlay(dragging?.id == d.id && isUpdating ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = d
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging, updating: $isUpdating))
}
}.animation(.default, value: model.data)
}
.border(.red)
.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging))
}
}
Problem:
when dropping the cell outside of the view, overlay does not reset.
In this case above the animation show a drop on the navigation bar. Not sure why, but the gif changed the color to black instead of the red, for the scrollview border.
How can detect the drop outside of the view so the overlay can be reset in this case?

How to create a SwiftUI button that changes opacity and triggers button action when touched?

I wanna create a button with SwiftUI that fires the moment my finger touches it (like UIKit's touch down instead of touch up inside). I also want the opacity of the button to become 0.7 when my finger is pressing the button. And I want the opacity of the button to change back to 1 ONLY when my finger is no longer touching the button.
I've tried 2 different types of button styles to create such a button but both of them failed:
struct ContentView: View {
var body: some View {
Button(action: {
print("action triggered")
}){
Text("Button").padding()
}
.buttonStyle(SomeButtonStyle())
}
}
struct SomeButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(Color.green)
.opacity(configuration.isPressed ? 0.7 : 1)
.onLongPressGesture(
minimumDuration: 0,
perform: configuration.trigger//Value of type 'SomeButtonStyle.Configuration' (aka 'ButtonStyleConfiguration') has no member 'trigger'
)
}
}
struct SomePrimativeButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(Color.green)
.opacity(configuration.isPressed ? 0.7 : 1)//Value of type 'SomePrimativeButtonStyle.Configuration' (aka 'PrimitiveButtonStyleConfiguration') has no member 'isPressed'
.onLongPressGesture(
minimumDuration: 0,
perform: configuration.trigger
)
}
}
Apparently none of the button styles above worked because ButtonStyle and PrimitiveButtonStyle don't share the same methods and properties so I can't use both the isPressed property (which belongs to ButtonStyle) AND the trigger method (which belongs to PrimitiveButtonStyle) in the same button style.
How should I configure my button style to make this work?
Ok, I understand that author wants to see solution only with Button, so I dig a little more. And found something interesting at Swift UI Lab. The idea is the same as in my first answer: use #GestureState and create LongPressGesture which .updating($...) this state. But in PrimitiveButtonStyle you don't need to compose a few gestures together. So, I simplified code a little and tested it at simulator. And I think now it just what author need:
struct ComposingGestures: View {
var body: some View {
Button(action: {
print("action triggered")
}){
Text("Button")
.padding()
}
.buttonStyle(MyPrimitiveButtonStyle())
}
}
struct MyPrimitiveButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}
struct MyButton: View {
#GestureState private var pressed = false
let configuration: PrimitiveButtonStyle.Configuration
let color: Color = .green
#State private var didTriggered = false
var body: some View {
// you can set minimumDuration to Double.greatestFiniteMagnitude if you think that
// user can hold button for such a long time
let longPress = LongPressGesture(minimumDuration: 300, maximumDistance: 300.0)
.updating($pressed) { value, state, _ in
state = value
self.configuration.trigger()
}
return configuration.label
.background(Color.green)
.opacity(pressed ? 0.5 : 1.0)
.gesture(longPress)
}
}
}
I didn't work with ButtonStyle, but tried to solve it with Composing SwiftUI Gestures. I compose TapGesture and LongPressGesture and playing with #GestureState to control .opacity of "button" (which is just Text). The result is just as you asked:
struct ComposingGestures: View {
enum TapAndLongPress {
case inactive
case pressing
var isPressing: Bool {
return self == .pressing
}
}
#GestureState var gestureState = TapAndLongPress.inactive
#State private var didPress = false
var body: some View {
let tapAndLongPressGesture = LongPressGesture(minimumDuration: 2) // if minimumDuration <= 1 gesture state returns to inactive in 1 second
.sequenced(before: TapGesture())
.updating($gestureState) { value, state, transaction in
switch value {
case .first(true), .second(true, nil):
self.didPress = true // simulation of firing action
state = .pressing
default:
state = .pressing
}
}
return VStack {
Text("action was fired!")
.opacity(didPress ? 1 : 0)
Text("Hello world!")
.gesture(tapAndLongPressGesture)
.background(Color.green)
.opacity(gestureState.isPressing ? 0.7 : 1)
}
}
}
P.S. I played only with #State var didPress to show, how to fire action. Maybe it's better to fire it only in the first case, like this:
// ...
.updating($gestureState) { value, state, transaction in
switch value {
case .first(true):
self.didPress = true // simulation of firing action
state = .pressing
case .second(true, nil):
state = .pressing
default:
state = .pressing
}
UPDATE
tried code at simulator and fixed two mistakes:
// ...
let tapAndLongPressGesture = LongPressGesture(minimumDuration: 300, maximumDistance: 300) // now button don't return opacity to 1 even if you move your finger
// ...
case .first(true), .second(true, nil):
DispatchQueue.main.async { // now there are no purple mistakes
self.didPress = true // simulation of firing action
}
state = .pressing
// ...