How to determine tap position on image in SwiftUI?

Is there any way to determine how to get the tap position (CGFloat) on an image View in SwiftUI, without resorting to UIKit?

I am trying to use purely SwiftUI to build a guitar you can tap to add notes. Using HStacks and VStacks of Views or elements results in performance issues (it has to draw 6 x 24 views often). So, I was going to try an approach using an image, determine where was tapped, and add an overlay at that location.

I am aware of one method using a drag gesture with minimum distance of 0, but I need the drag gesture to swipe between taps in a TabView.

Answered by EOTUmusic in 652281022
I'm new to coding, but here goes. This is a single guitar string. It has 24 "frets" - the GuitarFretView(). I then stack 5-9 of these strings in a VStack (adjustable via settings). The GuitarFretView is basically a line that adds a circle when you tap. Still working on it, will also need to update the data model when tapped.

struct GuitarStringView: View {

    let guitarString = Array(repeating: GuitarFretView(), count: 24)

    var body: some View {
        HStack {
            ForEach(0 ..< 24) { item in
                guitarString[item]
            }
        }
        .frame(maxHeight: 36, alignment: .center)
    }
}

struct GuitarFretView: View {

    @State var tapped = false

    var body: some View {
        ZStack {
            Path { path in
                path.move(to: CGPoint(x:0, y: 18))
                path.addLine(to: CGPoint(x: 50, y: 18))
            }
            .stroke(lineWidth: 3)
            .onTapGesture {
                withAnimation {
                tapped.toggle()
                }
            }

// appears when the string is tapped
            Circle()
                .fill(self.tapped ? Color.gray : Color.clear)
                .frame(width: 30, height: 30)
                .onTapGesture {
                    withAnimation {
                    tapped.toggle()
                    }
                }
        } // ZSTACK
    }
}


The method I found on Stack Overflow forums for getting the position is below. It worked, but then I was blocked from using the swipe for moving between tabs.

.simultaneousGesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
               .onEnded { print("X position \($0.location)") })

Thanks for any guidance!

Using HStacks and VStacks of Views or elements results in performance issues (it has to draw 6 x 24 views often).

Can you show your code using HStacks and VStacks? It might be easier to improve the performance of it.

I am aware of one method using a drag gesture with minimum distance of 0

Can you show the code of the method?
Accepted Answer
I'm new to coding, but here goes. This is a single guitar string. It has 24 "frets" - the GuitarFretView(). I then stack 5-9 of these strings in a VStack (adjustable via settings). The GuitarFretView is basically a line that adds a circle when you tap. Still working on it, will also need to update the data model when tapped.

struct GuitarStringView: View {

    let guitarString = Array(repeating: GuitarFretView(), count: 24)

    var body: some View {
        HStack {
            ForEach(0 ..< 24) { item in
                guitarString[item]
            }
        }
        .frame(maxHeight: 36, alignment: .center)
    }
}

struct GuitarFretView: View {

    @State var tapped = false

    var body: some View {
        ZStack {
            Path { path in
                path.move(to: CGPoint(x:0, y: 18))
                path.addLine(to: CGPoint(x: 50, y: 18))
            }
            .stroke(lineWidth: 3)
            .onTapGesture {
                withAnimation {
                tapped.toggle()
                }
            }

// appears when the string is tapped
            Circle()
                .fill(self.tapped ? Color.gray : Color.clear)
                .frame(width: 30, height: 30)
                .onTapGesture {
                    withAnimation {
                    tapped.toggle()
                    }
                }
        } // ZSTACK
    }
}


The method I found on Stack Overflow forums for getting the position is below. It worked, but then I was blocked from using the swipe for moving between tabs.

.simultaneousGesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
               .onEnded { print("X position \($0.location)") })

Thanks for any guidance!
Thanks for showing your code.

By the way, you marked this thread as SOLVED. Does that mean you have solved your issue by yourself?

If not, please read the followings.
  • Having an Array of Views is always not recommended in SwiftUI.

  •  then stack 5-9 of these strings in a VStack (adjustable via settings).

I cannot find any codes using VStack.
  • The method I found on Stack Overflow forums for getting the position is below. It worked, but then I was blocked from using the swipe for moving between tabs.

Where have you put the lines? And I cannot find any codes using the swipe for moving between tabs.


One more, you should better use the Code block feature available in the editor area of this site, indicated as < >.
Thanks. Yeah, I clicked the solved by accident then could not turn it off. I don't need a way to determine the position of the tap IF I can use the stacks method for Views. I will use one design or the other.

Ah, thanks for the pointer on the code block. I will use that.

For the VStack, imagine stacking the guitar strings:

Code Block Swift
VStack {
GuitarStringView()
GuitarStringView()
GuitarStringView()
GuitarStringView()
GuitarStringView()
GuitarStringView()
}

So now we have 6 GuitarStringViews times 24 of the GuitarFretViews. It might be OK if that was the only combination, but if you have one of these full "guitars" in each page of a TabView, or if you add/delete a string it is very slow to update the UI. I don't think this design will work.

if you have one of these full "guitars" in each page of a TabView, or if you add/delete a string it is very slow to update the UI.

Can you show the full code to reproduce the very slow updates.

I don't think this design will work.

View updating may be affected by many sorts of things. I cannot say if it will work or will not under the currently given info.
Thanks OOPer. I would rather not share ALL the code 😬😊. I was not aware that they don’t recommend generating views from arrays. I saw documentation that indicated that was what the ForEach was for. I know there is a limit of 10 child views so maybe generating 24 x 6 is too much.

If we go back to the original question, is there any way to get the tap location in SwiftUI or do you need to use UIKit?


is there any way to get the tap location in SwiftUI or do you need to use UIKit?

Nothing sure. Please share your solution when you solve this issue.

Hi a) In the case >= ios 16 use onTapGesture(count:coordinateSpace:perform:) b) In the case < ios 16 You can try

    TapGesture().sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded({ value in
                 print("Tap \(value.location)")
                })) 

Or implement based on UIKit UIView

How to determine tap position on image in SwiftUI?
 
 
Q