SwuftUI Gesture location detect clicks out of View border - swiftui

If I put several Views in a row with no spaces (or very little space between them), and attach some Gesture action, I can't correctly detect, which one is tapped. It looks like there is some padding inside gesture that I can't remove.
here's the example code:
struct ContentView: View {
#State var tap = 0
#State var lastClick = CGPoint.zero
var body: some View {
VStack{
Text("last tap: \(tap)")
Text("coordinates: (x: \(Int(lastClick.x)), y: \(Int(lastClick.y)))")
HStack(spacing: 0){
ForEach(0...4, id: \.self){ind in
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.gray)
.overlay(Text("\(ind)"))
.overlay(Circle()
.frame(width: 4, height: 4)
.foregroundColor(self.tap == ind ? Color.red : Color.clear)
.position(self.lastClick)
)
.frame(width: 40, height: 50)
//.border(Color.black, width: 0.5)
.gesture(DragGesture(minimumDistance: 0)
.onEnded(){value in
self.tap = ind
self.lastClick = value.startLocation
}
)
}
}
}
}
}
and behavior:
I want Gesture to detect 0 button clicked when click location goes negative. Is there a way to do that?

I spent few hours with that problem, and just after I post this question, I found solution.
it was so simple - just to add a contentShape modifier
...
.frame(width: 40, height: 50)
.contentShape(Rectangle())
.gesture(DragGesture(minimumDistance: 0)
...

Related

Have ScrollView always scrolled to bottom

I have a fixed height ScrollView containing a Text() which is constantly being updated from my viewModel.
If the text is too much to be viewed all at once, i.e. I need to scroll to see the end of the text, I’d like it to be automatically scrolled so that I always see the end of the text.
Is that possible?
ScrollView {
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
}
.frame(height: 200)
Note: this is a very simplified version of my problem. In my app there are times when the text is not being updated and it does need to be scrollable.
I have tried scrollViewReader … something like:
ScrollView {
ScrollViewReader() { proxy in
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
Text("").id(0)
}
}
.frame(height: 200)
with the idea of scrolling to the empty Text, but I couldn’t work out how to trigger
withAnimation {
proxy.scrollTo(0)
}
... all the examples I've seen use a button but I need to trigger when the text updates.
You can use .onChange to react to change of vm.text and then scroll to the end.
Please note that ScrollView should be inside the ScrollViewReader!
In principle it would work like this:
struct ContentView: View {
#State private var vmtext = "Test Text"
// timer change of text for testing
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Text(vmtext)
.id(0)
}
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
proxy.scrollTo(0, anchor: .bottom)
}
}
.frame(width: 100, height: 200, alignment: .leading)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
vmtext += " added new text"
}
}
}
The problem a have here is that it only works after the scrollview has been scrolled manually once.
I've adapted the code by #ChrisR with a hack that at the very least, shows how i'd like scrollTo: to work. It toggles between 2 almost identical views with different ids (one has a tiny bit of padding)
import SwiftUI
struct ContentView: View {
#State private var vmtext = "\n\n\n\nTest Text"
#State private var number = 0
#State private var id = 0
// timer change of text for testing
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
id == 0 ?
VStack {
Text(vmtext)
.padding(.top, 0)
.font(.title2)
.id(0)
}
:
VStack {
Text(vmtext)
.padding(.top, 1)
.font(.title2)
.id(1)
}
}
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
print("onChange entered. id: \(id)")
proxy.scrollTo(id, anchor: .bottom)
}
}
.frame(height: 90, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
if id == 0 { id = 1} else {id = 0}
number += 1
vmtext += " word\(number)"
}
}
}
Notes:
It seems that if the height of the text view being shown hasn't changed the proxy.scrollTo is ignored. Set the paddings the same and the hack breaks.
Removing the "\n\n\n\n" from the var vmtext breaks the hack. They make the initial size of the text view bigger than the scrollview window and so immediately scrollable - or something :-). If you do remove them, scrolling will start working after you do an initial scroll with your finger.
EDIT:
Here is a version without the padding and "\n\n\n\n" hacks, which uses a double rotation hack.
import SwiftUI
struct ContentView: View {
#State private var vmtext = "Test Text"
#State private var number = 0
#State private var id = 0
// timer change of text for testing
let timer = Timer.publish(every: 0.3, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Group {
id == 0 ?
VStack {
Text(vmtext)
.font(.title2)
.id(0)
}
:
VStack {
Text(vmtext)
.font(.title2)
.id(1)
}
}
.padding()
.rotationEffect(Angle(degrees: 180))
}
.rotationEffect(Angle(degrees: 180))
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
withAnimation {
if id == 0 { id = 1 } else { id = 0 }
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame(height: 180, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
number += 1
vmtext += " word\(number)"
}
}
}
It would be nice if the text started at the top of the view and only scrolls when the text has filled up the view, as with the swiftui TextEditor ... and the withAnimation doesn't work.

SwiftUI: Kingfisher KFImage flashing images within LazyHStack

KFImage within LazyHStask flashes image while dragging the view in spite of I set loadDiskFileSynchronously() to the view.
I suspect that KFImage loads image every time its onAppear() called, and it makes image flashing.
But it seems that KFImage preventing waste reloading image inside its onAppear().
https://github.com/onevcat/Kingfisher/blob/e30efd82368276664b9ffb8a10dc29cd6a6810da/Sources/SwiftUI/KFImageRenderer.swift#L75
How can I fix this?
This is sample snippet reproducing the problem.
struct SampleView : View {
let urls: [URL?]
#GestureState private var draggingOffset: CGFloat = 0
#State private var index: Int = 0
init(urls: [URL?]) {
self.urls = urls
}
var body: some View {
LazyHStack(spacing: 0) {
ForEach(urls.indices, id: \.self) { position in
KFImage(urls[position])
.resizable()
.loadDiskFileSynchronously()
.frame(width: UIScreen.main.bounds.width, alignment: .center)
.scaledToFit()
}
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height, alignment: .leading)
.offset(x: -CGFloat(index) * UIScreen.main.bounds.width)
.offset(x: draggingOffset)
.animation(.interactiveSpring(), value: index)
.animation(.interactiveSpring(), value: draggingOffset)
.gesture(
DragGesture()
.updating($draggingOffset) { value, state, _ in
state = value.translation.width
}
.onEnded { value in
let offset = value.translation.width / UIScreen.main.bounds.width
let newIndex = (CGFloat(index) - offset).rounded()
index = min(max(Int(newIndex), 0), urls.count - 1)
}
)
}
}
flashing images(gif)

SwiftUI - onTapGesture on HStack works, until it stops working

We have a custom view, that we use in forms (its main feature is to be able to show a nice border on if there is a validation error on the form):
struct ButtonNavigationLink: View {
var label: String
var markingType: MarkingType = .none
var action: () -> Void
var body: some View {
GeometryReader { geometry in
HStack {
Text(label)
.offset(x: 5, y: 0)
Spacer()
Image("PfeilRechts")
}
.frame(width: geometry.size.width + 10, height: geometry.size.height, alignment: .leading)
.padding(.trailing, 5)
.border(markingType.getMarkingColor())
.offset(x: -5, y: 0)
// to allow click on the whole width
.contentShape(Rectangle())
.onTapGesture {
print("in ButtonNavigationLink")
self.action()
}
}
}
}
The onTapGesture works well, until it doesn't work anymore.
It happens if we open other sheets and then eventually come to this one.
The funny thing is, that - when is not working anymore - if you scroll, even so lightly, the form containing it, it starts working again. The form is not disabled

SwiftUI - How to initialize only one SectionView in ScrollView using onTapGesture

My problem is that when user presses on the item in my ScrollView all of the SectionView items changes. What I want to do is to change only one item. I'm trying to change item image when user taps on it and then when user taps on another item to bring back previous item image and change the current item image.
Take a look at my ScrollView:
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 15) {
ForEach(1..<41) { item in
GeometryReader { geometry in
SectionView(number: item, pressed: pressed, firstElement: (item == 1) ? true : false)
.rotation3DEffect(Angle(degrees:
Double(geometry.frame(in: .global).minX - 30) / -20
), axis: (x: 0, y: 10, z: 0))
.onTapGesture {
self.pressed.toggle()
SectionView(number: item, pressed: self.pressed, firstElement: (item == 1) ? true : false)
print("touched item \(item)")
}
})
}
}
}
Here's my SectionView:
struct SectionView: View {
var number: Int
var pressed: Bool
var firstElement: Bool
var body: some View {
ZStack(alignment: .bottom) {
Image(pressed ? "t1" : "t\(number)")
.resizable()
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
.aspectRatio(contentMode: .fill)
Spacer()
Text(firstElement ? "" : "Pride")
.font(.system(size: 12, weight: .black))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.bottom, 15)
}
.frame(width: 60, height: 80)
//.background(section.image)
.cornerRadius(7)
//.shadow(color: section.image.opacity(0.15), radius: 8, x: 0, y: 10)
}
}
I tried putting your code into some kind of buildable state, but I couldn't get it to compile or run, so I may be completely misinterpreting what you are trying to do. However, it seems to me you are using pressed as if you are capturing the state at the time of the press on that particular SectionView. However, it would have to be a State var to compile, so it will update all of the views, which is what I believe you are seeing. I think what you need to do is change pressed to be an Int that keeps track of the SectionView that was pressed, and compare it to number which lets the particular SectionView keep track of its identity.
Also, I don't think you want the SectionView in the tap gesture. I think you just want to change the state of the pressed variable and let it work its way through the hierarchy.

How to stop SwiftUI DragGesture from dying when View content changes

In my app, I drag a View horizontally to set a position of a model object. I can also drag it downward and release it to delete the model object. When it has been dragged downward far enough, I indicate the potential for deletion by changing its appearance. The problem is that this change interrupts the DragGesture. I don't understand why this happens.
The example code below demonstrates the problem. You can drag the light blue box side to side. If you pull down, it and it turns to the "rays" system image, but the drag dies.
The DragGesture is applied to the ZStack with a size of 50x50. The ZStack should continue to exist across that state change, no? Why is the drag gesture dying?
struct ContentView: View {
var body: some View {
ZStack {
DraggableThing()
}.frame(width: 300, height: 300)
.border(Color.black, width: 1)
}
}
struct DraggableThing: View {
#State private var willDeleteIfDropped = false
#State private var xPosition: CGFloat = 150
var body: some View {
//Rectangle()
// .fill(willDeleteIfDropped ? Color.red : Color.blue.opacity(0.3))
ZStack {
if willDeleteIfDropped {
Image(systemName: "rays")
} else {
Rectangle().fill(Color.blue.opacity(0.3))
}
}
.frame(width: 50, height: 50)
.position(x: xPosition, y: 150)
.gesture(DragGesture()
.onChanged { val in
print("drag changed \(val.translation)")
self.xPosition = 150 + val.translation.width
self.willDeleteIfDropped = (val.translation.height > 25)
}
.onEnded { val in
print("drag ended")
self.xPosition = 150
}
)
}
}
You need to keep content, which originally captured gesture. So your goal can be achieved with the following changes:
ZStack {
Rectangle().fill(Color.blue.opacity(willDeleteIfDropped ? 0.0 : 0.3))
if willDeleteIfDropped {
Image(systemName: "rays")
}
}