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

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?

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: Rearrange items in Scrollable LazyVGrid with Draggable and DropDestination

I'm trying to rearrange or move items in a LazyVGrid inside a ScrollView using .draggable and .dropDestination view modifier based on the post here.
My problem is that I need to know which item is being dragged and particularly when the user stops dragging the item, much like onEnded for DragGesture. This works fine when the item is dropped inside a view with a .dropDestination but if the user drops it outside the item get "stuck" as being the draggedItem. See the video:
Drag and Drop outside of view
Is there a way to tell when the item is dropped, regardless of where it's dropped?
Here's my code currently which only works if item is dropped within the views with .dropDestination.
import SwiftUI
import UniformTypeIdentifiers
extension UTType {
static var itemTransferable = UTType(exportedAs: "com.styrka.DragableGrid.item")
}
struct ItemDraggable: Identifiable, Equatable, Transferable, Codable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: ItemDraggable.self, contentType: .itemTransferable)
}
var id: Int
}
struct MainView: View {
let columns = [
GridItem(.fixed(160)),
GridItem(.fixed(160))
]
#State private var items = (0..<20).map { ItemDraggable(id: $0) }
#State private var draggingItem: ItemDraggable?
var body: some View {
ScrollView {
Text("Items, dragging: \(draggingItem?.id ?? -1)")
LazyVGrid(columns: columns) {
ForEach(items) { item in
DraggableView(item: item, draggingItem: $draggingItem)
}
}
}
.background(Color.white)
.dropDestination(for: ItemDraggable.self) { items, location in
// User to drop items outside but does not cover the whole app
draggingItem = nil
return true
}
}
}
struct DraggableView: View {
var item: ItemDraggable
#Binding var draggingItem: ItemDraggable?
#State private var borderColor: Color = .black
#State private var borderWidth: CGFloat = 0.0
var body: some View {
Text("\(item.id)").font(.caption)
.frame(width: 100, height: 100)
.background(RoundedRectangle(cornerRadius: 16).fill(.blue).opacity(0.6))
.border(borderColor, width: borderWidth)
.opacity(item == draggingItem ? 0.1 : 1)
.draggable(item, preview: {
Text("Dragging item \(item.id)")
.onAppear {
// Set binding when draggable Preview appears..
draggingItem = item
}
.onDisappear{
// Called as soon as the dragged item leaves the 'DraggedView' frame
//draggingItem = nil
}}
)
.dropDestination(for: ItemDraggable.self) { items, location in
draggingItem = nil
return true
} isTargeted: { inDropArea in
borderColor = inDropArea ? .accentColor : .black
borderWidth = inDropArea ? 10.0 : 0.0
}
}
}

Can I set the text color of individual items in a SwiftUI Segmented Picker?

I have a segmented picker in SwiftUI. I'd like each element in the picker to have its own color.
I've tried this:
struct ContentView: View {
#State private var selectedColorIndex = 0
var body: some View {
VStack {
Picker("Favorite Color", selection: $selectedColorIndex, content: {
Text("Red").foregroundColor(.red).tag(0)
Text("Green").tag(1).foregroundColor(.green)
Text("Blue").tag(2).foregroundColor(.blue)
})
.pickerStyle(.segmented)
Text("Selected color: \(selectedColorIndex)")
}
}
}
but the items in the picker don't change color. I find this strange, since items are just simple Text views.....

SwiftUI animation not working using animation(_:value:)

In SwiftUI, I've managed to make a Button animate right when the view is first drawn to the screen, using the animation(_:) modifier, that was deprecated in macOS 12.
I've tried to replace this with the new animation(_:value:) modifier, but this time nothing happens:
So this is not working:
struct ContentView: View {
#State var isOn = false
var body: some View {
Button("Press me") {
isOn.toggle()
}
.animation(.easeIn, value: isOn)
.frame(width: 300, height: 400)
}
}
But then this is working. Why?
struct ContentView: View {
var body: some View {
Button("Press me") {
}
.animation(.easeIn)
.frame(width: 300, height: 400)
}
}
The second example animates the button just as the view displays, while the first one does nothing
The difference between animation(_:) and animation(_:value:) is straightforward. The former is implicit, and the latter explicit. The implicit nature of animation(_:) meant that anytime ANYTHING changed, it would react. The other issue it had was trying to guess what you wanted to animate. As a result, this could be erratic and unexpected. There were some other issues, so Apple has simply deprecated it.
animation(_:value:) is an explicit animation. It will only trigger when the value you give it changes. This means you can't just stick it on a view and expect the view to animate when it appears. You need to change the value in an .onAppear() or use some value that naturally changes when a view appears to trigger the animation. You also need to have some modifier specifically react to the changed value.
struct ContentView: View {
#State var isOn = false
//The better route is to have a separate variable to control the animations
// This prevents unpleasant side-effects.
#State private var animate = false
var body: some View {
VStack {
Text("I don't change.")
.padding()
Button("Press me, I do change") {
isOn.toggle()
animate = false
// Because .opacity is animated, we need to switch it
// back so the button shows.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
animate = true
}
}
// In this case I chose to animate .opacity
.opacity(animate ? 1 : 0)
.animation(.easeIn, value: animate)
.frame(width: 300, height: 400)
// If you want the button to animate when the view appears, you need to change the value
.onAppear { animate = true }
}
}
}
Follow up question: animating based on a property of an object is working on the view itself, but when I'm passing that view its data through a ForEach in the parent view, an animation modifier on that object in the parent view is not working. It won't even compile. The objects happen to be NSManagedObjects but I'm wondering if that's not the issue, it's that the modifier works directly on the child view but not on the passed version in the parent view. Any insight would be greatly appreciated
// child view
struct TileView: View {
#ObservedObject var tile: Tile
var body: some View {
Rectangle()
.fill(tile.fillColor)
.cornerRadius(7)
.overlay(
Text(tile.word)
.bold()
.font(.title3)
.foregroundColor(tile.fillColor == .myWhite ? .darkBlue : .myWhite)
)
// .animation(.easeInOut(duration: 0.75), value: tile.arrayPos)
// this modifier worked here
}
}
struct GridView: View {
#ObservedObject var game: Game
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 4)
var body: some View {
GeometryReader { geo in
LazyVGrid(columns: columns) {
ForEach(game.tilesArray, id: \.self) { tile in
Button(action: {
tile.toggleSelectedStatus()
moveTiles() <- this changes their array position (arrayPos), and
the change in position should be animated
}) {
TileView(tile: tile)
.frame(height: geo.size.height * 0.23)
}
.disabled(tile.status == .solved || tile.status == .locked)
.animation(.easeInOut(duration: 0.75), value: arrayPos)
.zIndex(tile.status == .locked ? 1 : 0)
}
}
}
}
}

How to initialize a binding Bool value in a SwiftUI view

Revised Example
Based on the thoughtful response from #pawello2222 I received in the comments below I have revised an example to demonstrate the issue I am wrestling with.
In order to demonstrate the issue I am having I am using 2 views a parent and a child. In my code code the parent view executes multiple steps but sometimes the animation subview is not visible in the first step. However when it does become visible the animation has already taken on the appearance of the end state. You can see this behavior in the following example.
Parent View
struct ContentView: View {
#State var firstTime = true
#State var breath = false
var body: some View {
VStack {
// This subview is not displayed until after the first time
if !firstTime {
SecondView(breath: $breath)
}
Spacer()
// A button click simulates the steps in my App by toggling the #Binding var
Button("Breath") {
withAnimation {
self.breath.toggle()
self.firstTime = false
}
}
// This vies shows what happens when the subview is being displayed with an intial state of false for the #Binding var
Spacer()
SecondView(breath: $breath)
}
}
}
Here is the subview containing the animation and using a #Binding var to control the animation appearance.
struct SecondView: View {
#Binding var breath: Bool
var body: some View {
Image(systemName: "flame")
.resizable()
.rotationEffect(.degrees(breath ? 360 : 0), anchor: .center)
.scaleEffect(breath ? 1 : 0.2)
.opacity(breath ? 1 : 0.75)
.animation(.easeInOut(duration: 2))
.foregroundColor(Color.red)
}
}
When you execute this the first time thru the top subview is not being displayed and when you click the button the lower subview executes the expected animation and then toggles the firstTime var so that the top subview becomes visible. Notice that the animation is fully expanded and if I was to then have another step (click) with the same value of true for the #Binding property the view would not change at all. This is the issue I am wrestling with. I would like to keep the subview from being at the end state if the first step is one that has toggled the Bool value even if the subview was not being displayed. In other words I would like to only initialize the subview when it is actually being displayed with a value of true so that the animation will always start out small.
This is why I was hoping to have the subview initialized the Binding var to false until it actually gets invoked for the first time (or to reset its state to the shrunk version of the animation) whichever is more feasible.
It looks like you may want to initialise _breath with the provided parameter:
struct ContentView: View {
#Binding var breath: Bool
init(breath: Binding<Bool>) {
_breath = breath
}
}
However, if you want to use a constant value (in your example false) you can do:
struct ContentView: View {
#Binding var breath: Bool
init(breath: Binding<Bool>) {
_breath = .constant(false)
}
}
But then, why do you need the breath: Binding<Bool> parameter?
EDIT
Here is an example how to control an animation of a child view using a #Binding variable:
struct ContentView: View {
#State var breath = false
var body: some View {
VStack {
Button("Breath") {
withAnimation {
self.breath.toggle()
}
}
SecondView(breath: $breath)
}
}
}
struct SecondView: View {
#Binding var breath: Bool
var body: some View {
Image(systemName: "flame")
.imageScale(.large)
.rotationEffect(.degrees(breath ? 360 : 0), anchor: .center)
.scaleEffect(breath ? 1 : 0.2)
.opacity(breath ? 1 : 0.75)
.animation(.easeInOut(duration: 2))
}
}