I am also learning SwiftUI. Here is my initial solution. The button needs a stoke outline which I did not handle since I am putting the button on a green background.
I am not sure if this is the best way to do it. It duplicates what I did in my Swift application so I did not spend more time on refining it.
It uses buttonStyle property since I have many buttons.
In my application, I named "MyButtonStyle" as "GameButtonStyle" since I am using it on a menu View for game actions.
struct ExampleView: View {
var body: some View {
ZStack {
Color(red: 0, green: 0.5, blue: 0)
VStack {
Button("Button") {
print("tap button")
}
.buttonStyle(MyButtonStyle(labelWidth: 150, labelHeight: 30, scaleFactor: 1))
}
}
}
}
struct MyButtonStyle: ButtonStyle {
let labelWidth:CGFloat
let labelHeight:CGFloat
let scaleFactor:CGFloat
let minWidth:CGFloat
let verticalPadding:CGFloat
init(labelWidth: CGFloat, labelHeight: CGFloat, scaleFactor: CGFloat, minWidth: CGFloat = 0, verticalPadding: CGFloat = 0) {
self.labelWidth = labelWidth
self.labelHeight = labelHeight
self.scaleFactor = scaleFactor
self.minWidth = minWidth
self.verticalPadding = verticalPadding
}
func makeBody(configuration: Configuration) -> some View {
let darkBlue = Color(red: 0, green: 0, blue: 1)
configuration.label
.frame(width: labelWidth, height: labelHeight)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.font(Font.system(size: (7 * labelHeight) / 9))
.frame(minWidth: minWidth)
//.padding(.horizontal, 10)
.padding(.vertical, verticalPadding)
.foregroundColor(configuration.isPressed ? Color.white : Color.black)
.background(configuration.isPressed ? darkBlue : Color.white)
.cornerRadius(labelHeight)
}
}
Post
Replies
Boosts
Views
Activity
In the given example, the last line is needed (hitButtonNode = nil). This could be a source of error, especially when there are many items to reset.
Since GestureState automatically handles the resetting of the state, it would be nice if it wasn't reset before onEnded is started (this is my original note). I am not sure why it is being reset before .onEnded starts. My surmise is that Apple intends the gestureState to be used to change the appearance of some other item within the view (like its highlighting or its color). But, I don't see why this would require the gestureState to be reset before beginning .onEnded.
This last comment leads to another comment about .onChange and .onEnded for gestures. It is not clear to me whether the mouseUp event that triggers .onEnded would first call .onChange and then call .onEnded. It would be nice if it was documented (and actually done) this way. Since I am not sure, I usually make the code in .onChange a single function and when .onEnded starts I call that function to insure that I am following the last mouse or tap event.
I may have been misusing what GestureState was intended for.
I came up with another approach that seems easier for what I want.
This is what I want:
I have a SpriteView with a GameScene (subclassed from SKScene).
I want the SpriteView to handle the gestures (since it is way easier than the old approach).
I want the tapped buttonNode in the GameScene to track the location for highlighting and unhighlighting.
When the tracking ends, I want the tapped buttonNode to do its action if it is still being selected.
But I cannot attach the SwiftUI gestures to the SKNode buttonNodes of the GameScene since they are not Views.
Thus, I have to have a custom SwiftUI dragGesture to handle the above.
The major problem is to remember the originally touched buttonNode.
Instead of trying to use GestureState, I just use a locally captured variable within the "var" or "function" gesture.
Here is a modified version of what I do. The modification uses force unwrapping to keep it simple.
I put the dragGesture as part of the GameScene rather than as part of the file with the SpriteView.
This is because of the needs for my particular program.
The code can be simplified if you put it with the file with the SpriteView. For example, you can eliminate the guard statements for gameScene.
The major point of the example is to how to capture the "begin" state of the gesture without using GestureState.
extension GameScene {
var buttonNodeGesture: some Gesture {
var hitButtonNode:Button3Node?
return DragGesture(minimumDistance: 0)
.onChanged { [weak self] gesture in
guard let gameScene = self else { return }
let buttonNodes = gameScene.buttonNodes
let firstButtonNode = buttonNodes.first!
let parent = firstButtonNode.parent!
do { // updating stuff
if hitButtonNode == nil {
let startLocation = gameScene.convert(fromGesture: gesture.startLocation, to: parent)
hitButtonNode = gameScene.buttonNodes.filter{$0.contains(startLocation)}.first
}
}
do { // onChanged stuff
if let hitButtonNode = hitButtonNode {
let location = gameScene.convert(fromGesture: gesture.location, to: parent)
if hitButtonNode.contains(location) {
hitButtonNode.highlight()
}
else {
hitButtonNode.unhighlight()
}
}
}
}
.onEnded { [weak self] gesture in
guard let gameScene = self else { return }
let buttonNodes = gameScene.buttonNodes
let firstButtonNode = buttonNodes.first!
let parent = firstButtonNode.parent!
do { // onEnded stuff
if let hitButtonNode = hitButtonNode {
hitButtonNode.unhighlight()
let location = gameScene.convert(fromGesture: gesture.location, to: parent)
if hitButtonNode.contains(location) {
hitButtonNode.doButtonAction()
}
}
}
hitButtonNode = nil // Note: probably not needed
}
}
}