SwiftUI - Respond to tap AND double tap with different actions - swiftui

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

Related

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())
}
}

How to Push View or Present View Modally in SwiftUI dynamically?

I'm learning SwiftUI and my focus at the moment is to implement a method which I'm able to implement using UIKit. I need to create a method the determines whether I should Push the View or Present modally the view based on the value of a boolean.
In UIKit, my code is:
var presentVC = true // boolean that determines whether VC will be presented or pushed
let vc = ViewController() //Your VC that will be pushed or presented
if (presentVC == true) {
self.presentViewController(vc, animated: true, completion: nil)
} else {
self.navigationController.pushViewController(vc, animated: true)
}
But in SwiftUI, I'm not sure how to implement this correctly using the:
NavigationLink - For pushing the View
.sheet(isPresented:, content:) - For presenting the View modally
It seems that the NavigationLink and .sheet modifier is coupled with the view implementation.
Does anyone already encountered and solve this scenario in SwiftUI? Thanks
I'm using SwiftUI 1.0 as I need to support iOS 13.
A possible solution is to create a custom enum with available presentation types:
enum PresentationType {
case push, sheet // ...
}
and create a custom binding for activating different views:
func showChildView(presentationType: PresentationType) -> Binding<Bool> {
.init(
get: { self.showChildView && self.presentationType == presentationType },
set: { self.showChildView = $0 }
)
}
Full code:
struct ContentView: View {
#State var presentationType = PresentationType.push
#State var showChildView = false
func showChildView(as presentationType: PresentationType) -> Binding<Bool> {
.init(
get: { self.showChildView && self.presentationType == presentationType },
set: { self.showChildView = $0 }
)
}
var body: some View {
NavigationView {
VStack {
Button(action: {
self.presentationType = .push
self.showChildView = true
}) {
Text("Present new view as Push")
}
Button(action: {
self.presentationType = .sheet
self.showChildView = true
}) {
Text("Present new view as Sheet")
}
}
.navigationBarTitle("Main view", displayMode: .inline)
.background(
NavigationLink(
destination: ChildView(),
isActive: self.showChildView(presentationType: .push),
label: {}
)
)
}
.sheet(isPresented: self.showChildView(presentationType: .sheet)) {
ChildView()
}
}
}
struct ChildView: View {
var body: some View {
ZStack {
Color.red
Text("Child view")
}
}
}

How can I use multiple fullScreenCover in IOS14

I want to present the two destinations view in full screen mode from a single view.
Below is a sample of my code. Seem that the function only works for single presentation, if I have a second fullScreenCover defined, the first fullScreenCover didn't work properly.Is that any workaround at this moment?
import SwiftUI
struct TesFullScreen: View {
init(game : Int){
print(game)
}
var body: some View {
Text("Full Screen")
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
.fullScreenCover(isPresented: self.$showFullScreen1){
TesFullScreen(game: 1)
}
.fullScreenCover(isPresented: self.$showFullScreen2){
TesFullScreen(game: 2)
}
}
}
Not always the accepted answer works (for example if you have a ScrollView with subviews (cells in former days) which holds the buttons, that set the navigational flags).
But I found out, that you also can add the fullScreen-modifier onto an EmptyView. This code worked for me:
// IMPORTANT: Has to be within a container (e.g. VStack, HStack, ZStack, ...)
if myNavigation.flag1 || myNavigation.flag2 {
EmptyView().fullScreenCover(isPresented: $myNavigation.flag1)
{ MailComposer() }
EmptyView().fullScreenCover(isPresented: $myNavigation.flag2)
{ RatingStore() }
}
Usually some same modifier added one after another is ignored. So the simplest fix is to attach them to different views, like
struct FullSContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
.fullScreenCover(isPresented: self.$showFullScreen1){
Text("TesFullScreen(game: 1)")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
.fullScreenCover(isPresented: self.$showFullScreen2){
Text("TesFullScreen(game: 2)")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
}
}
Alternate is to have one .fullScreenCover(item:... modifier and show inside different views depending on input item.
The only thing that worked for me was the answer in this link:
https://forums.swift.org/t/multiple-sheet-view-modifiers-on-the-same-view/35267
Using the EmptyView method or other solutions always broke a transition animation on one of the two presentations. Either transitioning to or from that view and depending on what order I chose them.
Using the approach by Lantua in the link which is using the item argument instead of isPresented worked in all cases:
enum SheetChoice: Hashable, Identifiable {
case a, b
var id: SheetChoice { self }
}
struct ContentView: View {
#State var sheetState: SheetChoice?
var body: some View {
VStack {
...
}
.sheet(item: $sheetState) { item in
if item == .a {
Text("A")
} else {
Text("B")
}
}
}
}
The sheetState needs to be optional for it to work.

SwiftUI NavigationLink double click on List MacOS

Can anyone think how to call an action when double clicking a NavigationLink in a List in MacOS? I've tried adding onTapGesture(count:2) but it does not have the desired effect and overrides the ability of the link to be selected reliably.
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: Item(itemDetail: item)) {
ItemRow(itemRow: item) //<-my row view
}.buttonStyle(PlainButtonStyle())
.simultaneousGesture(TapGesture(count:2)
.onEnded {
print("double tap")
})
}
}
}
}
EDIT:
I've set up a tag/selection in the NavigationLink and can now double or single click the content of the row. The only trouble is, although the itemDetail view is shown, the "active" state with the accent does not appear on the link. Is there a way to either set the active state (highlighted state) or extend the NavigationLink functionality to accept double tap as well as a single?
#State var selection:String?
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: Item(itemDetail: item), tag: item.id, selection: self.$selection) {
ItemRow(itemRow: item) //<-my row view
}.onTapGesture(count:2) { //<- Needed to be first!
print("doubletap")
}.onTapGesture(count:1) {
self.selection = item.id
}
}
}
}
}
Here's another solution that seems to work the best for me. It's a modifier that adds an NSView which does the actual handling. Works in List even with selection:
extension View {
/// Adds a double click handler this view (macOS only)
///
/// Example
/// ```
/// Text("Hello")
/// .onDoubleClick { print("Double click detected") }
/// ```
/// - Parameters:
/// - handler: Block invoked when a double click is detected
func onDoubleClick(handler: #escaping () -> Void) -> some View {
modifier(DoubleClickHandler(handler: handler))
}
}
struct DoubleClickHandler: ViewModifier {
let handler: () -> Void
func body(content: Content) -> some View {
content.background {
DoubleClickListeningViewRepresentable(handler: handler)
}
}
}
struct DoubleClickListeningViewRepresentable: NSViewRepresentable {
let handler: () -> Void
func makeNSView(context: Context) -> DoubleClickListeningView {
DoubleClickListeningView(handler: handler)
}
func updateNSView(_ nsView: DoubleClickListeningView, context: Context) {}
}
class DoubleClickListeningView: NSView {
let handler: () -> Void
init(handler: #escaping () -> Void) {
self.handler = handler
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if event.clickCount == 2 {
handler()
}
}
}
https://gist.github.com/joelekstrom/91dad79ebdba409556dce663d28e8297
I've tried all these solutions but the main issue is using gesture or simultaneousGesture overrides the default single tap gesture on the List view which selects the item in the list. As such, here's a simple method I thought of to retain the default single tap gesture (select row) and handle a double tap separately.
struct ContentView: View {
#State private var monitor: Any? = nil
#State private var hovering = false
#State private var selection = Set<String>()
let fruits = ["apple", "banana", "plum", "grape"]
var body: some View {
List(fruits, id: \.self, selection: $selection) { fruit in
VStack {
Text(fruit)
.frame(maxWidth: .infinity, alignment: .leading)
.clipShape(Rectangle()) // Allows the hitbox to be the entire word not the if you perfectly press the text
}
.onHover {
hovering = $0
}
}
.onAppear {
monitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) {
if $0.clickCount == 2 && hovering { // Checks if mouse is actually hovering over the button or element
print("Double Tap!") // Run action
}
return $0
}
}
.onDisappear {
if let monitor = monitor {
NSEvent.removeMonitor(monitor)
}
}
}
}
This works if you just need to single tap to select and item, but only do something if the user double taps. If you want to handle a single tap and a double tap, there still remains the problem of single tap running when its a double tap. A potential work around would be to capture and delay the single tap action by a few hundred ms and cancel it if it was a double tap action
Use simultaneous gesture, like below (tested with Xcode 11.4 / macOS 10.15.5)
NavigationLink(destination: Text("View One")) {
Text("ONE")
}
.buttonStyle(PlainButtonStyle()) // << required !!
.simultaneousGesture(TapGesture(count: 2)
.onEnded { print(">> double tap")})
or .highPriorityGesture(... if you need double-tap has higher priority
Looking for a similar solution I tried #asperi answer, but had the same issue with tappable areas as the original poster. After trying many variations the following is working for me:
#State var selection: String?
...
NavigationLink(destination: HistoryListView(branch: string), tag: string, selection: self.$selection) {
Text(string)
.gesture(
TapGesture(count:1)
.onEnded({
print("Tap Single")
selection = string
})
)
.highPriorityGesture(
TapGesture(count:2)
.onEnded({
print("Tap Double")
})
)
}

SwiftUI - how to detect long press on Button?

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