I have a SwiftUI button where initially I handled the button-press inside the action area, but now I want to change the button so that I can also long press it. In order to do this, I now have to create an onTapGesture and onLongPressGesture. After testing my app on my Watch, I realised that the button is less responsive with onTapGesture and onLongpressGesture. I can visibly see the button being pressed down, but it only sometimes processes action 1 and action 2. Is anyone having a similar experience, what is the workaround?
Button(action: {
// action 1
}) {
Image(systemName: "checkmark")
.onTapGesture (count:1){
// action 1
}
.onLongPressGesture {
// action 2
}
}
don't use button.
Image(systemName: "checkmark")
.onTapGesture {
// action 1
}
.onLongPressGesture {
// action 2
}
Or add simultanous gesture to button
Button(action: {
print("tap")
}) {
Image(systemName: "trash")
}.simultaneousGesture(LongPressGesture(minimumDuration: 1, maximumDistance: 10).onEnded({ (b) in
print("long")
}))
warning, 'extra' tap will follow each long event ...
You can play with gesture mask, let say
Button(action: {
print("tap")
}) {
Image(systemName: "trash")
}.simultaneousGesture(LongPressGesture().onChanged({ (b) in
print("long change", b)
})
.onEnded({ (b) in
print("long end")
}), including: .gesture)
prints
long change true // on tap
or
// on long press
long change true
long end
but no "tap" !
Related
My app has a number of mainButton created by a single struct. The data is passed to these buttons from an array. Then I have a Submit button.
I am trying to have the Submit button to change label to a arrow up icon only when: a given instance of mainButton has been pressed once already and changed its color and size already (this works), and Submit was pressed once already. Now the Submit button's icon changes to a refresh symbol (arrow up), until a diffrent instance of mainButton has been pressed (and this new button in turn has changed the color and size already as expected).
Basically a button representing a value is selected(tapped) and then its content submitted by pressing the Submit button, and if you submit the same button again the submitter is a refresher (arrow up icon) rather than a submitter (Submit label) as you have submitted that given query before; hence if you want to submit the same value again, the button is submit button is labeled as a refresher. The code below should make this clearer.
I am new to Swift/SwiftUI and trying to do this in a simple way as I want to understand it fully. Hence, if possible, I would rather use if conditions and variables rather than advanced methods.
Here is a simplified version of my code with only place holders in it:
//
// AddView.swift
// WriteOn
//
// Created by Maurizio Zappettini on 2/11/23.
//
import SwiftUI
struct AddView: View {
#State private var typeofMessageGeneric: String = ""
#State private var lastButtonTapped: String = ""
#State private var buttonCount: Int = 0
let buttonData = [
(mainButtonText: "A", mainButtonValue: "a"),
(mainButtonText: "B", mainButtonValue: "b"),
(mainButtonText: "C", mainButtonValue: "c"),
(mainButtonText: "D", mainButtonValue: "d"),
]
var body: some View {
ZStack { // Using ZStack for background gradient
Rectangle()
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading)
.foregroundColor(.clear)
.background(LinearGradient(colors: [.blue, .purple], startPoint: .top, endPoint: .bottom))
VStack { //Master VStack
VStack { //Headline
Text("Test View")
.font(.headline)
.padding()
}
GeometryReader { geometry in // Debug alignment
VStack(alignment: .center) { //TextBox
Rectangle()
.fill(Color.cyan)
.cornerRadius(20)
.frame(width: geometry.size.width * 0.8, height: 250, alignment: .center)
.overlay(
HStack(alignment: .top){
VStack {
Spacer()
}
ScrollView {
VStack(alignment: .center) {
Text("getResponsePlaceHolder")
.foregroundColor(.black)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.layoutPriority(2)
}
}
VStack{
ShareLink(item: "nothing"){
Image(systemName: "square.and.arrow.up")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(.black)
}
}
VStack{
Spacer()
}
}
.frame(height: geometry.size.height * 0.6) // find way to make it refer to actual box and not whole screen
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} //Vstack Top
} //Geo Reader
Spacer(minLength: 50
)
ScrollView{
VStack { //Buttons
ForEach(buttonData, id: \.mainButtonValue) { data in
mainButton(typeofMessageGeneric: self.$typeofMessageGeneric, lastButtonTapped: self.$lastButtonTapped, buttonCount: self.$buttonCount, mainButtonText: data.mainButtonText, mainButtonValue: data.mainButtonValue)
}
} //Button Vstack end
}
VStack{ //Footer
submitButton()
}
}
}
}
func submitButton() -> some View {
return Button(action: getResponse) {
if self.typeofMessageGeneric == self.lastButtonTapped && self.buttonCount == 0 {
Text("Submit")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
} else {
Image(systemName: "arrow.clockwise.circle")
.font(.system(size: 30))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
}
// Create main buttons
struct mainButton: View {
#Binding var typeofMessageGeneric: String // Bind the variable inside this struct with the same one inside the ContentView struct
#Binding var lastButtonTapped: String // Not currently used -- implement later if needed
#Binding var buttonCount:Int
var mainButtonText:String
var mainButtonValue:String
var body: some View {
Button(action: {
self.typeofMessageGeneric = mainButtonValue
self.lastButtonTapped = mainButtonValue
buttonCount = 0
if self.typeofMessageGeneric == mainButtonValue{
buttonCount = 1
} else {
buttonCount = 0
}
}) {
Text(mainButtonText)
Text(String(buttonCount)) // testing only
}
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(self.mainButtonValue == self.typeofMessageGeneric ? Color.teal : Color.purple)
.cornerRadius(10)
.scaleEffect(self.mainButtonValue == self.typeofMessageGeneric ? 1.12 : 1)
.animation(.easeOut, value: 2)
}
}
func getResponse() {
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView()
}
}
EDIT:
Here is a better explanation of what the app does and what I am trying to achieve in terms of button logic.
What's on the screen when you start the app:
There is an empty text box.
There are 4 buttons (it will be more)
There is a Submit button at the bottom
What the app does:
The 4 main buttons are 4 different prompts. Each prompt/button is composed of a label and an associated value.
Tapping a button acts as a way to select that button's corresponding value.
Tapping the submit button will submit the value of the last selected main buttons to an external API. The latter will return some specific data that is visualized in the text box above (this part I left out as it is unrelated to my button label problem).
If the same prompt is submitted again the API will return different data (as there is a randomization mechanism). Hence, the user may want to submit again the same prompt they have submitted already.
How the buttons behave (practical example):
1- You tap button B
2- Button B changes the background color from purple to teal and becomes bigger.
3- You tap the "Submit" button.
4- The submit button changes its label from "Submit" to ARROW_UP.
5- The app submits value "b" to the API and this returns DATA_b_1.
6- DATA_b_1 is visualized in the text box.
7- Button B is still 'selected' (it is still teal and larger)
8- You tap ARROW_UP.
9- Value "b" is submitted again to the API and this now returns
DATA_b_2 (a different random instance of DATA_b).
10- DATA_b_2 is visualized in the text box.
11- You now tap button D.
12- Button D changes the background color from purple to teal and becomes bigger.
13- Button B reverts its color to purple and its size to smaller.
14- ARROW_UP button reverts its label to "Submit".
15- Back to point 3
Everything in the list above works as expected except the "Submit" to ARROW_UP and vice-versa label changes.
I hope I understood what you are trying to achieve, the. question is not very clear to me. What I got is:
Show the question and 4 possible answers to the user. Let the user select an answer. Submit button: disabled.
The user selects one answer; they can change their mind as much as they want until the answer is submitted. Submit button: enabled.
The user pressed "Submit": they can't press any other button, except for "Refresh". Submit button: "arrow up".
The user pressed "Refresh": go back to 1.
If that's the case, you just need to track two things:
what is the selected answer (when none is selected, nothing can be submitted)
when the answer was submitted - then, the only option is to refresh the question
By the way, your variable buttonCount is pointless: if you have only one variable for the whole view, all buttons will show the same number.
With a good set of ifs and elses, the "Submit" button can be disabled, enabled or show "arrow up", and perform the right action. Here's the code:
// Track which button was pressed - or none of them (starting case)
#State private var buttonPressed: String = ""
// Track if a choice was submitted - when submitted, the Submit button can only refresh
#State private var isWaitingToRefresh = false
// For testing only - tests the refresh
#State private var question = "0"
// (other code)
var body: some View {
// (other code)
Text("getResponsePlaceHolder \(question)")
// (other code)
ScrollView{
VStack { //Buttons
ForEach(buttonData, id: \.mainButtonValue) { data in
// Call a function, no need for binding vars
mainButton(mainButtonText: data.mainButtonText,
mainButtonValue: data.mainButtonValue)
}
} //Button Vstack end
}
submitButton()
}
}
}
func submitButton() -> some View {
Button {
// No action if no button was pressed
guard !buttonPressed.isEmpty else { return }
// Check if you need to refresh the question
if isWaitingToRefresh {
// If the view is waiting to refresh, then refresh the question
refresh()
isWaitingToRefresh = false
} else {
// If the view is not waiting to refresh, submit the user's choice
isWaitingToRefresh = true
getResponse()
}
} label: {
if !isWaitingToRefresh {
Text("Submit")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
// Gray-out the button if the user made no choice yet
.background(buttonPressed.isEmpty ? Color.secondary : Color.blue)
.cornerRadius(10)
} else {
Image(systemName: "arrow.clockwise.circle")
.font(.system(size: 30))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
// Disable the button if the user made no choice yet
.disabled(buttonPressed.isEmpty)
}
// Create main buttons
// Call a function, no need for binding vars:
// you can use the same variables of the view
func mainButton(mainButtonText: String,
mainButtonValue: String) -> some View {
Button {
// You just need to change the choice when a button is pressed,
// let the Submit button do the rest
buttonPressed = mainButtonValue
} label: {
Text(mainButtonText)
Text(String("same number for all buttons")) // testing only
}
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(mainButtonValue == buttonPressed ? Color.teal : Color.purple)
.cornerRadius(10)
.scaleEffect(mainButtonValue == buttonPressed ? 1.12 : 1)
.animation(.easeOut, value: 2)
// Do not let the user change the choice if it was already submitted
.disabled(isWaitingToRefresh)
}
// Remember to reset the user's choice (buttonPressed = "")
func refresh() {
buttonPressed = ""
question = String(Int.random(in: 1...100))
}
I have a List of cells. Each cell includes a button, which, when tapped, shows a popover.
Button(action: { showingPopover = true },
label: { Image(systemName: "gearshape") })
.buttonStyle(BorderlessButtonStyle())
.popover(isPresented: $showingPopover,
arrowEdge: .leading) {
AccessoryIconView(viewModel: viewModel.iconViewModel())
.frame( width: 200, height: 300)
}
The problem is that, when the popover is shown, and I tap the button of another cell, the current popover is not dismissing and the new popover does not show, with this message in the console :
Attempt to present <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x10460cab0> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x104006f60> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVVS_22_VariadicView_Children7ElementGVS_18StyleContextWriterVS_19SidebarStyleContext___: 0x104218480>)
which is already presenting <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x104409b30>.
How can I tell SwiftUI to first properly dismiss the shown popover before showing the new one ?
I have a SwiftUI List on iOS14.3 (Xcode12.3) and inside it's cells I want to have a Menu popup up when the user is tapping on a button. Additionally these cells have an onTapGesture and the problem is that this gesture is also triggered when the menu button is pressed.
Example:
List {
HStack {
Text("Cell content")
Spacer()
// Triggers also onTapGesture of cell
Menu(content: {
Button(action: { }) {
Text("Menu Item 1")
Image(systemName: "command")
}
Button(action: { }) {
Text("Menu Item 2")
Image(systemName: "option")
}
Button(action: { }) {
Text("Menu Item 3")
Image(systemName: "shift")
}
}) {
Image(systemName: "ellipsis")
.imageScale(.large)
.padding()
}
}
.background(Color(.systemBackground))
.onTapGesture {
print("Cell tapped")
}
}
If you tap on the ellipsis image, the menu opens but also "Cell tapped" will be printed to the console. This is a problem for me, because in my real world example I am collapsing the cell within the tap gesture and of course I don't want that to happen when the user presses the menu button.
I found out, that when I long press on the button the gesture won't be triggered. But this is not acceptable UX in my opinion, took me a while to find that out for my self.
I also noticed, when I replace the Menu with a regular Button, then the cells gesture is not triggered while pressing (only with BorderlessButtonStyle or PlainButtonStyle, otherwise he is not active at all). But I don't know how to open a Menu from it's action.
List {
HStack {
Text("Cell content")
Spacer()
// Does not trigger cells onTapGesture, but how to open Menu from action?
Button(action: { print("Button tapped") }) {
Image(systemName: "ellipsis")
.imageScale(.large)
.padding()
}
.buttonStyle(BorderlessButtonStyle())
}
.background(Color(.systemBackground))
.onTapGesture {
print("Cell tapped")
}
}
Alternatively, you can override Menu onTapGesture:
struct ContentView: View {
var body: some View {
List {
HStack {
Text("Cell content")
Spacer()
Menu(content: {
Button(action: {}) {
Text("Menu Item 1")
Image(systemName: "command")
}
Button(action: {}) {
Text("Menu Item 2")
Image(systemName: "option")
}
Button(action: {}) {
Text("Menu Item 3")
Image(systemName: "shift")
}
}) {
Image(systemName: "ellipsis")
.imageScale(.large)
.padding()
}
.onTapGesture {} // override here
}
.contentShape(Rectangle())
.onTapGesture {
print("Cell tapped")
}
}
}
}
Also, there's no need to use .background(Color(.systemBackground)) to tap on spacers. This is not really flexible - what if you want change background color?
You can use contentShape instead:
.contentShape(Rectangle())
I'm not sure if this is a bug or expected behavior, but instead of fighting with it I would recommend to avoid such overlapping (because even with success today it might stop working on different systems/updates).
Here is possible solution (tested with Xcode 12.1 / iOS 14.1)
List {
HStack {
// row content area
HStack {
Text("Cell content")
Spacer()
}
.background(Color(.systemBackground)) // for tap on spacer area
.onTapGesture {
print("Cell tapped")
}
// menu area
Menu(content: {
Button(action: { }) {
Text("Menu Item 1")
Image(systemName: "command")
}
Button(action: { }) {
Text("Menu Item 2")
Image(systemName: "option")
}
Button(action: { }) {
Text("Menu Item 3")
Image(systemName: "shift")
}
}) {
Image(systemName: "ellipsis")
.imageScale(.large)
.padding()
}
}
}
I was testing accuracy of recognizing tap Gesture of a simple Button in SwiftUI, which I noticed it get activated even outside of its frame! I wanted be sure that I am not messing with label or other thing of Button, there for I build 2 different Buttons to find any improvement, but both of them get triggered in outside of given frame. I made a gif for showing issue.
gif:
code:
struct ContentView: View {
var body: some View {
Spacer()
Button("tap me!") {
print("you just tapped Button1!")
}.font(Font.largeTitle.bold()).background(Color.red)
Spacer()
Button(action: {
print("you just tapped Button2!")
}, label: {
Text("tap me!").font(Font.largeTitle.bold()).background(Color.red)
})
Spacer()
}
}
update:
some one said:
Do you see circle on click? - It is a size of active tap spot (kind of finger), and as you see, from your own demo, it overlaps red square - so gesture detected. That's it
I made another gif to show it is completely wrong to thinking like that, in this gif, even with overlaps tap get not registered!
I don't have an answer why this happens other than maybe there is some built-in functionality that increases the tappable area where possible for a better user experience? Anyway, if you put another tappable button behind it or next to it, you'll notice that this no longer happens and the tappable area is exactly where you'd expect it. Therefore, I wouldn't worry about it. But if you need to clip the tap area to the exact frame, you could add a clear background to the button and add a tap event that doesn't doesn't do anything, but takes priority on that location.
var body: some View {
VStack {
// Button behind button will take priority tap.
ZStack {
Button(action: {
print("tap 2")
}, label: {
Text("tap me!")
.padding()
.font(Font.largeTitle.bold())
.background(Color.green)
})
Button(action: {
print("tap 1")
}, label: {
Text("tap me!")
.font(Font.largeTitle.bold())
.background(Color.red)
})
}
// Clear background with "fake" tap event
Button(action: {
print("tap 3")
}, label: {
Text("tap me!")
.font(Font.largeTitle.bold())
.background(Color.red)
})
.padding()
.background(Color.black.opacity(0.001).onTapGesture {
//print("tap 4")
})
}
}
I think it's a built-in functionality to expand the tappable area:
Color.red
.frame(width: 30.0, height: 30.0)
.gesture(DragGesture(minimumDistance: 0).onEnded({ (value) in
// Tap with Apple Pencil: location is exactly the frame
// Tap with finger or an capacitance pen: the hit location is inaccurate
print(value.startLocation)
}))
Solution (Bug: If the view is inside a ScrollView, this will block scroll gesture):
let size = CGSize(width: 200.0, height: 200.0)
Color.red
.frame(width: size.width, height: size.height)
.gesture(DragGesture(minimumDistance: 0).onEnded({ (value) in
print(value.startLocation)
if value.startLocation.x >= 0 && value.startLocation.y >= 0 && value.startLocation.x <= size.width && value.startLocation.y <= size.height {
print("inside frame")
// action ...
} else {
print("outside frame")
}
}))
I'm trying SwiftUI tutorial in apple developer page. now I'm following transition tutorial but my transition is not working when the view added.
here my code.
VStack(alignment: .leading) {
HStack() {
// title
Text(titleText)
.font(.headline)
.padding()
Spacer()
// button
Button(action: {
withAnimation {
self.showDetail.toggle()
}
}) {
Image(systemName: "chevron.right.circle")
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.padding()
}
}
// detail
if showDetail {
Text(contentText)
.transition(.slide)
.padding()
}
}
I think the Text that has contentText, should appear with slide transition but when I press the Button, it just pop up. but when I press button again it disappear with transition. so removal transition is work.
How can I fix this?
After some test I found some solution.
solution 1:
It is just bug of XCode canvas.
It works in the simulator or on a real machine.
solution 2. (if you really want to do in canvas)
Button (action : {
withAnimation(.easeInOut) { // add animation manually
self.showDetail.toggle()
}
}) {
// label Image no change
}
if showDetail {
Text(contentText)
.transition(.slide)
.animation(.easeInOut) // match with withAnimation
}
Although I haven't done the tutorial you are talking about but I think it works pretty fine using the .move transition instead of the .slide transition.
Here's my code for the detail part (the rest is unchanged). I hope my answer can help you
if showDetail {
Text(contentText)
.transition(.move(edge: .leading))
.padding()
}