SwiftUI - how to detect long press on Button? - swiftui

I have a Button where when it gets pressed, it performs some actions. But I would like to modify the same Button to detect a longer press, and perform a different set of processes. How do I modify this code to detect a long press?
Button(action: {
// some processes
}) {
Image(systemName: "circle")
.font(.headline)
.opacity(0.4)
}

Here is possible variant (tested with Xcode 11.2 / iSO 13.2).
Button("Demo") {
print("> tap")
}
.simultaneousGesture(LongPressGesture().onEnded { _ in
print(">> long press")
})

Daniel Wood's comment is correct in both statements. First, it is the cleanest solution. For the second, there is a straightforward workaround.
struct ContentView: View {
#State private var didLongPress = false
var body: some View {
Button("Demo") {
if self.didLongPress {
self.didLongPress = false
doLongPressThing()
} else {
doTapThing()
}
}.simultaneousGesture(
LongPressGesture().onEnded { _ in self.didLongPress = true }
)
}
}

This way you can also recognize a long press on a button and distinguish it from a simple tap gesture:
Button(action: {}) {
Text("Push")
}
.simultaneousGesture(TapGesture().onEnded { _ in
print("simultaneousGesture TapGesture")
})
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("simultaneousGesture LongPressGesture")
})

As of iOS 15 you should use a Menu with a primaryAction

Related

SwiftUI: Conditional Context Menu Shown Unexpectedly

In the following SwiftUI view, why does the conditional .contextMenu not work correctly?
Steps:
Long press the list item
Tap Edit
Long press the list item again
On the second long press the context menu should not appear because editMode?.wrappedValue is .active. But it does appear. How to fix that?
struct ContentView: View {
#Environment(\.editMode) private var editMode
var body: some View {
let contextMenu = ContextMenu {
Button("Do nothing", action: {})
}
VStack(alignment: .leading, spacing: 12) {
EditButton().padding()
List {
ForEach(1..<2) {i in
Text("Long press me. Editing: \((editMode?.wrappedValue == .active).description)")
.contextMenu(editMode?.wrappedValue == .active ? nil : contextMenu)
}
.onDelete(perform: { _ in })
.onMove(perform: { _, _ in })
}
Spacer()
}
}
}
Works fine with Xcode 14 / iOS 16
Here is possible workaround for older versions (it is possible to try different places for .id modifier to have appropriate, acceptable, UI feedback)
Tested with Xcode 13.4 / iOS 15.5
Text("Long press me. Editing: \((editMode?.wrappedValue == .active).description)")
.contextMenu(editMode?.wrappedValue == .active ? nil : contextMenu)
.id(editMode?.wrappedValue) // << this !!

SwiftUI: fullScreenCover with no animation?

I have this view:
struct TheFullCover: View {
#State var showModal = false
var body: some View {
Button(action: {
showModal.toggle()
}) {
Text("Show Modal")
.padding()
.foregroundColor(.blue)
}
.background(Color(.white))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.red, lineWidth:1)
)
.fullScreenCover(isPresented: $showModal, onDismiss: {
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
})
}
}
Every time I press the Button, the modal screen comes up fullscreen. All works great.
Question:
How do I disable the slide up animation? I want the view to be presented immediately fullscreen without animating to it.
Is there a way to do that?
A possible solution is to disable views animation completely (and then, if needed, enable again in .onAppear of presenting content), like
Button(action: {
UIView.setAnimationsEnabled(false) // << here !!
showModal.toggle()
}) {
and then
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
.onAppear {
UIView.setAnimationsEnabled(true) // << here !!
}
})
Tested with Xcode 13 / iOS 15
AFAIK the proper to do it as of today is using transaction https://developer.apple.com/documentation/swiftui/transaction
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
showModal.toggle()
}
I also created a handy extension for this:
extension View {
func withoutAnimation(action: #escaping () -> Void) {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
action()
}
}
}
which can be used like this:
withoutAnimation {
// do your thing
}
.fullScreenCover(isPresented: isPresented) {
content()
.background(TransparentBackground())
}
.transaction({ transaction in
transaction.disablesAnimations = true
})
this should work, based on #asamoylenko's answer
At the moment, I find it easier to use UIKit for presentation in SwiftUI.
someView
.onChange(of: isPresented) { _ in
if isPresented {
let vc = UIHostingController(rootView: MyView())
vc.modalPresentationStyle = .overFullScreen
UIApplication.shared.rootVC?.present(vc, animated: false)
} else {
UIApplication.shared.rootVC?.dismiss(animated: false)
}
}
Why not use an overlay instead?
.overlay {
if isLoading {
ZStack {
ProgressView()
}
.background(BackgroundCleanerView())
}
}

Detect "On Tap End" on Button

I'm developing a simple MIDI keyboard. Each piano key is a button. As soon you press it, it sends a "MIDI note ON" signal to a virtual device:
Button(action: {
MidiDevice.playNote("C")
}) {
Image(systemName: "piano-white-key")
}
It works fine. The latency is good and the user can play the key for just a fraction of a second or hold the button for longer notes. Now, how do I intercept the "user has lifted her finger" action in order to immediately send the MidiDevice.stopNote("C") event?
Here is possible solution (as far as I understood your goal) - to use ButtonStyle to detect isPressed state. Standard Button sends actions of tap UP, so we just add action handler for tap DOWN.
Tested with Xcode 12.4 / iOS 14.4
struct ButtonPressHandler: ButtonStyle {
var action: () -> ()
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(configuration.isPressed ?
Color.blue.opacity(0.7) : Color.blue) // just to look like system
.onChange(of: configuration.isPressed) {
if $0 {
action()
}
}
}
}
struct TestButtonPress: View {
var body: some View {
Button(action: {
print(">> tap up")
}) {
Image(systemName: "piano-white-key")
}
.buttonStyle(ButtonPressHandler {
print("<< tap down")
})
}
}
In addition to Asperi's answer, you can create an extension which will make it more SwiftUI-style:
extension Button {
func onTapEnded(_ action: #escaping () -> Void) -> some View {
buttonStyle(ButtonPressHandler(action: action))
}
}
struct ContentView: View {
var body: some View {
Button(action: {
print(">> tap up")
}) {
Image(systemName: "piano-white-key")
}
.onTapEnded {
print("<< tap down")
}
}
}

SwiftUI - Respond to tap AND double tap with different actions

I need to respond to a single tap and double tap with different actions, but is SwiftUI the double tap gesture is interpreted as two single taps.
In swift you can use fail gesture, but no idea how to do it in SwiftUI.
Example:
.onTapGesture(count: 1) {
print("Single Tap!")
}
.onTapGesture(count: 2) {
print("Double Tap!")
}
TIA.
First one prevent second one to perform. So reverse the order of your code:
.onTapGesture(count: 2) {
print("Double Tap!")
}
.onTapGesture(count: 1) {
print("Single Tap!")
}
Update: Second solution
Since the above method reported not working in some situationis, you can try using gestures modifier instead:
.gesture(TapGesture(count: 2).onEnded {
print("double clicked")
})
.simultaneousGesture(TapGesture().onEnded {
print("single clicked")
})
Text("Tap Me!").gesture(
TapGesture(count: 2)
.onEnded({ print("Tapped Twice!”)})
.exclusively(before:
TapGesture()
.onEnded({print("Tapped Once!”) })
)
)
There does not seem to be a simple solution (yet), most of these answers trigger both actions.
The workaround I have discovered feels clumsy, but works smoothly:
struct hwsView: View {
var body: some View {
VStack {
Text("HWS")
.onTapGesture {
print("Text single tapped")
}
}
.highPriorityGesture(
TapGesture(count: 2)
.onEnded { _ in
print("hws double tapped")
}
)
}
}
This can be achieved with exclusively(before:). It allows for only one gesture to succeed.
.gesture(
TapGesture(count: 2).onEnded {
print("DOUBLE TAP")
}.exclusively(before: TapGesture(count: 1).onEnded {
print("SINGLE TAP")
})
)
Link to method documentation https://developer.apple.com/documentation/swiftui/gesture/exclusively(before:)
I had a need for double and triple taps to be recognised on an object but only trigger one of the action closures. I found that the exclusively() API didn't work for me on iOS 15. So, I created this little hack that does what I want.
Struct ContentView: View {
#State private var isTripleTap = false
var body: some View {
Text("Button")
.gesture(
TapGesture(count: 2).onEnded {
Task {
try? await Task.sleep(nanoseconds: 200_000_000)
if !isTripleTap {
print("Double Tap")
}
isTripleTap = false
}
}
.simultaneously(
with: TapGesture(count: 3).onEnded {
isTripleTap = true
print("Triple Tap")
}
)
)
}
}
You can also setup a count for TapGesture like so:
.simultaneousGesture(TapGesture(count: 2).onEnded {
// double tap logic
})
Per Apple Developer Documentation:
https://developer.apple.com/documentation/swiftui/tapgesture

Disable drag to dismiss in SwiftUI Modal

I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! 🙌🏼")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out 😂
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}