SwiftUI: Sequence gestures starting with tap - swift

I want to create the "one finger zoom gesture" from the iOS Maps App. So a tap (touch down, then touch up), then long press (only touch down), and finally a drag up/down.
I was expecting something like this:
let tapGesture = TapGesture().onEnded { _ in
print("tap")
}
let longPressGesture = LongPressGesture(minimumDuration: 0.00001)
.onChanged { _ in
print("long press update")
}
.onEnded { _ in
print("long press")
}
let dragGesture = DragGesture(minimumDistance: 1)
.onChanged { gesture in
print("drag")
}
let combined = tapGesture.sequenced(before: longPressGesture).sequenced(before: dragGesture)
SomeView()
.gesture(combined)
Only the LongPressGesture after the initial tap never gets called. I've tried a LongPressGesture instead of the initial TapGesture, but two sequential long presses are called instantly (so the "touch up" event is not regarded).
An initial DragGesture(minimumDuration: 0) with onEnded and a corresponding #State to track the initial tap might work, but I have a ScrollView beneath (for the map-like panning and zooming), so I'm guessing a DragGesture cannot be the first one.
I also tried setting a gestureEnabled flag after a first tap via onTapGesture() and then using the .gesture(combined, including: gestureEnabled ? .all : .none), with a few more edge cases handled I managed to get it working, only to find out that double taps no longer worked anywhere else on the View.
I should clarify that there's a ScrollView with text underneath the view, so long press, double tap, scrolling (horizontal and vertical) should all keep working.
Any ideas?
Currently, my best alternative is just using the long press + drag. It works with the additional tap as well (since it's not necessary in that case). Only downside is it also triggers if the user holds down for a short time before attempting a scroll.

Related

SwiftUI iOS - Adding Double tap cause Single tap called after a delay

I want to recognize Single tap and Double tap on the Capsule below. This code works fine :
Capsule()
.frame(width: 100, height: 42)
.onTapGesture(count: 1) {
print("Single Tap recognized instantly")
}
But When I'm adding the .onTapGesture(count: 2) to it Single tap called after 0.25ms.
Capsule()
.frame(width: 100, height: 42)
.onTapGesture(count: 2) {
print("Double tap recognized instantly")
}
.onTapGesture(count: 1) {
print("Single Tap recognized after 0.25ms")
}
How can I fix this?
Just to make one thing clear: You cannot have a single tap-gesture that should be recognized together with a double tap gesture when your goal is to detect both exclusively (i.e. the system should differentiate between a single tap and a double tap with no delay) since a single tap could theoretically always lead to a double tap. This means you always have to wait a certain period (of 0.25 ms as it seems in your case) to rule out the user intended to double-tap.
However, when your goal is to have a single tap-gesture together (in other words simultaneously) with the double-tap gesture, this is now completely feasible.
A gesture is, like so many things in SwiftUI a protocol and custom gestures can be built in a lot of ways like the body of a view. In your case, we want a double tap-gesture that runs simultaneously with a single tap gesture. The simplest way to this, you declare a computed property with an opaque return type (some Gesture) and describe your gesture in that property:
var tapGesture: some Gesture {
TapGesture(count: 2)
.onEnded {
print("Double tap")
}
.simultaneously(with: TapGesture(count: 1)
.onEnded {
print("Single Tap")
})
}
You can simply add this gesture to whatever view you desire, just like you would do the same thing with any other gesture:
Capsule()
.gesture(tapGesture)

Postponing gesture recognizer in swift (UISwipeGestureRecognizer)

I have a swipe gesture recognizer that I turn off after the user swipes. That’s when I have a series of animations. 1st, a view will move in the direction that the user swipes. Then based on the location that it stops, another animation occurs based on the location that it stopped in. I then turn on the gesture recognizer all the way at the end of the function that handles this swipe. The problem is that the recognizer is turning on too quickly and therefore if the user were to quickly swipe in another direction, the animation would only occur in the wrong place (due to the fact that the view is in a different place). My question is, how can I create a function that waits X amount of time and then turns back on the recognizer?
// 1
var toDoSmth: (()->void)? = nil
// 2
toDoSmth = {
// turns back on the recognizer
// and do any delayed task
}
// 3
let delayedTime = DispatchTime.now() + .seconds(1)
DispatchQueue.main.asyncAfter(deadline: delayedTime) {
toDoSmth()
}
Here: 1, 2 - created variable/autoclosure;
3 - run your code after 1 second.

#IBSegueAction with condition

In my app I want to perform a segue based on a UITapGestureRecognizer. In case the tap is in the top area of the screen, a segue to the SettingsView should be performed.
In UIKit this was quite simple. I've triggered the performSegue in the UITapGestureRecognizer by wrapping it in an if-statement.
Now I would like to write the SettingsView() in SwiftUI. The SettingsView() will be embedded in a UIHostingController.
My question is:
How can I perform the segue to this UIHostingController (while telling the UIHostingController which View to display)?
I tried to use the new #IBSegueAction. The reason to use this #IBSegueAction is that I can use it to tell the UIHostingController which View to display. The problem is that I can't insert a condition now. Wherever the tap is on the screen, the segue is performed. I haven't found a way to cancel the segue in #IBSegueAction.
My code currently looks like this:
#IBSegueAction func showSettingsHostingControler(_ coder: NSCoder, sender: UITapGestureRecognizer, segueIdentifier: String?) -> UIViewController? {
let location = sender.location(in: self.tableView)
if let _ = self.tableView.indexPathForRow(at: location) {
return nil
} else {
if location.y < 32 {
if location.x < view.bounds.midX {
direction = .left
} else {
direction = .right
}
return UIHostingController(coder: coder, rootView: SettingsView())
}
}
return nil
}
The result currently is that the app segues to a non-existing Nil-view when the tap is in the wrong area.
based on a UITapGestureRecognizer. In case the tap is in the top area of the screen
It sounds to me like your tap gesture recognizer is attached to the wrong view. There should not be any decision to make here. Position a view in the top area of the screen and attach the tap gesture recognizer to that. That way, if this view gets a tap, there is no decision to be made: the tap is in the right place.

Detecting UIButton tap for button inside of tappable view

I have a container view with a button over it which hides and shows the view. Within the shown view, there are N number of mini buttons that have actions.
The problem I'm having is, when I tap on the mini buttons, those targets are ignored and the larger view button is what receives the action.
How do I configure things so that the larger tappable button on the view still works in most places but where the mini buttons exist, those tap actions register as well?
Thanks!
There are two possible solution
First
Change view hierarchy of uibutton (large on top of the stack in
interface builder)
Like
-Largebutton
-minibutton1
-minibutton1
'
'
-minibuttonn
Second one
Use gesture on the conainer view like
let hideViewGesture = UITapGestureRecognizer(target: self, action: "hideView")
containerView.addGestureRecognizer(hideViewGesture)
func hideView() {
containerView.isHidden = true
}
Its not merely possible to get button action working within a button as
1) Adding a button [Large button] on ContainerView will cause always to detect Large button action and will not allow you to detect button inside it
2) If seen in case of layers large button layer is on top So Logically always large button will first come in Action not inside View of containerView
Possible Solutions :
1) try to make use of gestures on ContainerView
2) you can use a segmented control as show and hide or a UIButton that is placed on side of containerView not over it So you will be able to perform all the required Actions
This is an old question but there actually is a simple solution, by overriding hitTest(_ point: CGPoint, with event: UIEvent?).
Inside your outer button:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.innerButton.frame.contains(point) {
return self.innerButton
}
else if self.bounds.contains(point) {
return self
}
else {
return super.hitTest(point, with: event)
}
}
It tests to see if the touch point is within the inner button (or you could have several inner buttons), if so it returns it and the touch is registered only by the inner button, if not it goes to the outer view. If neither view contains it, it calls super so the touch is handled correctly by other views.

Add an animation to swipe gestures in Swift

I have a swipe gesture in my app that when you swipe, it changes the text on screen. How would I make it so it looks like it is sliding to the new text rather than just instantly changing the text?
You could do one thing!!!add a view behind your gesture view ,that view should look same as that of gesture View(should even have same text),and once after recognizing gesture bring that view in-front of your gesture view it should have old text(don't update the text) and update your gesture view's text ones it moves behind newly added view, just change the frame of newly added view so that it gives a sliding kind of effect(change its width) once after the completing animation bring that view back to gesture view and change its frame to previous its previous value.
check this sample:
func handleGesture(sender:UISwipeGestureRecognizer) {
//bring background view to front
self.view.bringSubviewToFront(self.backGroundView)
self.gestureView.userInteractionEnabled = false
//capture its initialAutolayout constraint
let backgroundViewSpaceConstarint = self.backGroundViewLeftSpaceConstraint.constant
UIView.animateWithDuration(2, animations: { [unowned self]() -> Void in
self.someRandomValue++
// update text label in gesture view which is behind background View
self.gestureViewTextLabel.text = "swiped \(self.someRandomValue) times"
//slide backgroundView in required direction by using autolayout
self.backGroundViewLeftSpaceConstraint.constant = self.backGroundView.bounds.size.width
self.backGroundView.layoutIfNeeded()
}) { [unowned self](completion:Bool) -> Void in
//after animation complition bring gesture view to the front.
self.backgroundTextLabel.text = "swiped \(self.someRandomValue) times"
self.gestureView.userInteractionEnabled = true
//upadte background view so that it will look the same for next swipe
self.backGroundViewLeftSpaceConstraint.constant = backgroundViewSpaceConstarint
//send the background view behind gesture view
self.view.sendSubviewToBack(self.backGroundView)
}
}