8

Short version: How do I get the coordinates of a clicked Button in SwiftUI?

I'm looking for something like this (pseudo code) where geometry.x is the position of the clicked button in the current view:

GeometryReader { geometry in      
     return Button(action: { self.xPos = geometry.x}) {  
          HStack {               
               Text("Sausages")  
          }  
     }  
}

Long version: I'm beginning SwiftUI and Swift so wondering how best to achieve this conceptually.

To give the concrete example I am playing with:

Imagine a tab system where I want to move an underline indicator to the position of a clicked button.

[aside] There is a answer in this post that visually does what I am going for but it seems rather complicated: How to make view the size of another view in SwiftUI [/aside]

Here is my outer struct which builds the tab bar and the rectangle (the current indicator) I am trying to size and position:


import SwiftUI
import UIKit

struct TabBar: View {

    var tabs:ITabGroup
    @State private var selected = "Popular"
    @State private var indicatorX: CGFloat = 0
    @State private var indicatorWidth: CGFloat = 10
    @State private var selectedIndex: Int = 0

    var body: some View {
        ScrollView(.horizontal) {
            HStack(spacing: 0) {
                ForEach(tabs.tabs) { tab in
                    EachTab(tab: tab, choice: self.$selected, tabs: self.tabs, x: self.$indicatorX, wid: self.$indicatorWidth)
                }
            }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 40, maxHeight: 40).padding(.leading, 10)
                .background(Color(UIColor(hex: "#333333")!))
            Rectangle()
                .frame(width: indicatorWidth, height: 3 )
                .foregroundColor(Color(UIColor(hex: "#1fcf9a")!))
            .animation(Animation.spring())
        }.frame(height: 43, alignment: .leading)
    }
}

Here is my struct that creates each tab item and includes a nested func to get the width of the clicked item:

struct EachTab: View {
    // INCOMING!
    var tab: ITab
    @Binding var choice: String
    var tabs: ITabGroup
    @Binding var x: CGFloat
    @Binding var wid: CGFloat
    @State private var labelWidth: CGRect = CGRect()

    private func tabWidth(labelText: String, size: CGFloat) -> CGFloat {
        let label = UILabel()
        label.text = labelText
        label.font = label.font.withSize(size)
        let labelWidth = label.intrinsicContentSize.width
        return labelWidth
    }

    var body: some View {
        Button(action: { self.choice = self.tab.title; self.x = HERE; self.wid = self.tabWidth(labelText: self.choice, size: 13)}) {
            HStack {
                // Determine Tab colour based on whether selected or default within black or green rab set
                if self.choice == self.tab.title {
                    Text(self.tab.title).foregroundColor(Color(UIColor(hex: "#FFFFFF")!)).font(.system(size: 13)).padding(.trailing, 10).animation(nil)
                } else {
                    Text(self.tab.title).foregroundColor(Color(UIColor(hex: "#dddddd")!)).font(.system(size: 13)).padding(.trailing, 10).animation(nil)
                }
            }
        }
        // TODO: remove default transition fade on button click
    }
}

Creating a non SwiftUI UILabel to get the width of the Button seems a bit wonky. Is there a better way?

Is there a simple way to get the coordinates of the clicked SwiftUI Button?

Ben Frain
  • 2,510
  • 1
  • 29
  • 44

3 Answers3

6

You can use a DragGesture recogniser with a minimum drag distance of 0, which provides you the location info. However, if you combine the DragGesture with your button, the drag gesture won't be triggered on normal clicks of the button. It will only be triggered when the drag ends outside of the button.

You can get rid of the button completely, but of course then you lose the default button styling.

The view would look like this in that case:

struct MyView: View {

    @State var xPos: CGFloat = 0

    var body: some View {
        GeometryReader { geometry in
            HStack {
                Text("Sausages: \(self.xPos)")
            }
        }.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { dragGesture in
            self.xPos = dragGesture.location.x
        })
    }
}

The coordinateSpace parameter specifies if you want the touch position in .local or .global space. In the local space, the position is relative to the view that you've attached the gesture to. For example, if I had a Text view in the middle of the screen, my local y position would be almost 0, whereas my global y would be half of the screen height.

This tripped me up a bit, but this example shows the idea:

struct MyView2: View {

    @State var localY: CGFloat = 0
    @State var globalY: CGFloat = 0

    var body: some View {
        VStack {
            Text("local y: \(self.localY)")
                .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded { dragGesture in
                    self.localY = dragGesture.location.y
                })

            Text("global y: \(self.globalY)")
                .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { dragGesture in
                    self.globalY = dragGesture.location.y
                })
        }
    }
}
omar
  • 86
  • 1
  • 4
2
struct ContentView: View {

var body: some View {
    VStack {
        Button(action: { print("Button pressed")}) { Text("Button") }
    }.simultaneousGesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
        .onEnded { print("Changed \($0.location)") })
}
}

This solution seems to work, add a simultaneous gesture on Button, unfortunatelly it does not work if the Button is places in a Form

Sorin Lica
  • 6,894
  • 10
  • 35
  • 68
1

Turns out I solved this problem by adapting the example on https://swiftui-lab.com/communicating-with-the-view-tree-part-2/

The essence of the technique is using anchorPreference which is a means of sending data about one view back up the chain to ancestral views. I couldn't find any docs on this in the Apple world but I can attest that it works.

I'm not adding code here as the reference link also includes explanation that I don't feel qualified to re-iterate here!

Ben Frain
  • 2,510
  • 1
  • 29
  • 44
  • Anchor preferences is the way to go for this solution, IMHO. While I consider using DragGesture a workaround. Unfortunately, a solution with anchor preferences will become a little bit convoluted. – CouchDeveloper Aug 15 '21 at 20:17