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
?
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
})
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With