I have a table view that doesn't cover the whole screen (It's kind of like a drawer from the bottom of the screen). When the user scrolls down to the end of the content I want to stop the scrolling and then add a pan gesture recognizer. I do this like so:
// MARK: UIScrollViewDelegate Methods
extension TutorProfileVC: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Limit top vert bounce
guard mode == .drawer else { return }
if scrollView.contentOffset.y < -80.0 {
scrollView.contentOffset = CGPoint(x: 0, y: -80.0)
tableView.addGestureRecognizer(tablePanGR)
}
}
}
The gesture has been added but won't register until the user touches the screen again. Their finger is already on the tableview. Is it possible to start the gesture without them having to touch the screen again?
I think you have same problem with this question. Take a look at it if you want to see a code sample.
To resolve problem, you should add gesture from beginning but only handle gesture action when user scrolls to bottom. So you don't need to touch the screen again because gesture is started when you begin scrolling. The method to handle gesture will look like below
#objc func handlePanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
// Do nothing
break
case .changed:
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view!.superview)
let state = gestureRecognizer.state
// Don't do anything until |scrollView| reached bottom
if scrollView.contentOffset.y >= -80.0 {
return;
}
// Do whatever you want with |scrollView|
}
break;
case .cancelled:
case .ended:
case .failed:
default:
break;
}
}
Also implement gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: to make gesture and scroll view work together
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Related
I have a project where I’m adding three UILabels to the view controller’s view. When the user begins moving their finger around the screen, I want to be able to determine when they their finger is moving over any of these UILabels.
I’m assuming a UIPanGestureRecognizer is what I need (for when the user is moving their finger around the screen) but I’m not sure where to add the gesture. (I can add a tap gesture to a UILabel, but this isn’t what I need)
Assuming I add the UIPanGestureRecognizer to the main view, how would I go about accomplishing this?
if gesture.state == .changed {
// if finger moving over UILabelA…
// …do this
// else if finger moving over UILabelB…
// …do something else
}
You can do this with either a UIPanGestureRecognizer or by implementing touchesMoved(...) - which to use depends on what else you might be doing.
For pan gesture, add the recognizer to the view (NOT to the labels):
#objc func handlePan(_ g: UIPanGestureRecognizer) {
if g.state == .changed {
// get the location of the gesture
let loc = g.location(in: view)
// loop through each label to see if its frame contains the gesture point
theLabels.forEach { v in
if v.frame.contains(loc) {
print("Pan Gesture - we're panning over label:", v.text)
}
}
}
}
For using touches, no need to add a gesture recognizer:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let t = touches.first {
// get the location of the touch
let loc = t.location(in: view)
// loop through each label to see if its frame contains the touch point
theLabels.forEach { v in
if v.frame.contains(loc) {
print("Touch - we're dragging the touch over label:", v.text)
}
}
}
}
I have got an UITableView with a custom TableViewCell. I use a pan gesture for recognizing the positions while moving my finger to the left and to the right. On basis of the finger position I change some values in the labels in this TableViewCell. This works really great. But suddenly I can not scroll the TableView up and down. I already read the reason. Swift can not work with two gesture recognizers at the same time. And I found many examples of people how have nearly the same problem. I tried many of them but I can not fix my problem. I use Swift 5. Could you please describe a bit more precise how to fix my problem? Many thanks
import UIKit
class TVCLebensmittel: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
self.addGestureRecognizer(gestureRecognizer)
}
#IBAction func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began {
let translation = gestureRecognizer.translation(in: self)
// Put my finger on the screen
} else if gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self)
// While moving my finger ...
} else if gestureRecognizer.state == .ended {
let translation = gestureRecognizer.translation(in: self)
// Lift finger
}
}
...
}
The solution is to insert the pan gesture to the tableview and not to the tableviewcell. So I can listen to the left and right pan and also the up and down movement of the tableview.
I just share my approach. link It works very well. I needed custom swipe design while perform delete. Try it. If you need more information feel free to ask. Check it if you like.
This gestureRecognizerShouldBegin let you to use scroll while using UIPanGestureRecognizer.
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer.isKind(of: UIPanGestureRecognizer.self)) {
let t = (gestureRecognizer as! UIPanGestureRecognizer).translation(in: contentView)
let verticalness = abs(t.y)
if (verticalness > 0) {
print("ignore vertical motion in the pan ...")
print("the event engine will >pass on the gesture< to the scroll view")
return false
}
}
return true
}
I have been experimenting with custom interactive view controller presentation and dismissal (using a combination of UIPresentationController, UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning, and UIViewControllerTransitioningDelegate) and have mostly gotten things working well for my needs.
However, there is one common scenario that I've yet to find addressed in any of the tutorials or documentation that I've read, leading me to the following question:
...
What is the proper way of handling custom interactive view controller dismissal, via a pan gesture, when the dismissed view contains a UIScrollView (ie. UITableView, UICollectionView, WKWebView, etc)?
...
Basically, what I'd like is for the following:
View controllers are interactively dismissible by panning them down. This is common UX in many apps.
If the dismissed view controller contains a (vertically-scrolling) scroll view, panning down scrolls that view as expected until the user reaches the top, after which the scrolling ceases and the pan-to-dismiss occurs.
Scroll views should otherwise behave as normal.
I know that this is technically possible - I've seen it in other apps, such as Overcast and Apple's own Music app - but I've not been able to find the key to coordinating the behavior of my pan gesture with that of the scroll view(s).
Most of my own attempts center on trying to conditionally enable/disable the scrollview (or its associated pan gesture recognizer) based on its contentOffset.y while scrolling and having the view controller dismissal's pan gesture recognizer take over from there, but this has been fraught with problems and I fear that I am overthinking it.
I feel like the secret mostly lies in the following pan gesture recognizer delegate method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// ...
}
I have created a reduced sample project which should demonstrate the scenario more clearly. Any code suggestions are highly welcome!
https://github.com/Darchmare/SlidePanel-iOS
Solution
Make scrollView stop scrolling after it reached top by using UIScrollView's bounces property and scrollViewDidScroll(_:) method.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 10);
}
Don't forget to set scrollView.delegate = self
Only handle panGestureRecognizer when scrollView reached top - It means when scrollView.contentOffset.y == 0 by using a protocol.
protocol PanelAnimationControllerDelegate {
func shouldHandlePanelInteractionGesture() -> Bool
}
ViewController
func shouldHandlePanelInteractionGesture() -> Bool {
return (scrollView.contentOffset.y == 0);
}
PanelInteractionController
class PanelInteractionController: ... {
var startY:CGFloat = 0
private weak var viewController: (UIViewController & PanelAnimationControllerDelegate)?
#objc func handlePanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
break
case .changed:
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view!.superview)
let state = gestureRecognizer.state
// Don't do anything when |scrollView| is scrolling
if !(viewController?.shouldHandlePanelInteractionGesture())! && percentComplete == 0 {
return;
}
var rawProgress = CGFloat(0.0)
rawProgress = ((translation.y - startTransitionY) / gestureRecognizer.view!.bounds.size.height)
let progress = CGFloat(fminf(fmaxf(Float(rawProgress), 0.0), 1.0))
if abs(velocity.x) > abs(velocity.y) && state == .began {
// If the user attempts a pan and it looks like it's going to be mostly horizontal, bail - we don't want it... - JAC
return
}
if !self.interactionInProgress {
// Start to pan |viewController| down
self.interactionInProgress = true
startTransitionY = translation.y;
self.viewController?.dismiss(animated: true, completion: nil)
} else {
// If the user gets to a certain point within the dismissal and releases the panel, allow the dismissal to complete... - JAC
self.shouldCompleteTransition = progress > 0.2
update(progress)
}
case .cancelled:
self.interactionInProgress = false
startTransitionY = 0
cancel()
case .ended:
self.interactionInProgress = false
startTransitionY = 0
if self.shouldCompleteTransition == false {
cancel()
} else {
finish()
}
case .failed:
self.interactionInProgress = false
startTransitionY = 0
cancel()
default:
break;
}
}
}
Result
For more detail, you can take a look at my sample project
For me, this little bit of code answered a lot of my issues and greatly helped my custom transitions in scrollviews, it will hold a negative scrollview offset from moving while trying to start a transition or showing an activity indicator on the top. My guess is that this will solve at least some of your transition/animation hiccups:
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.contentOffset.y < -75 {
scrollView.contentInset.top = -scrollView.contentOffset.y
}
// Do animation or transition
}
I believe you don't need an additional pan gesture recognizer to implement this. You can simply hook onto the different delegate methods of the scroll view to achieve the "pan to dismiss" effect. Here is how I went about it
// Set the dragging property to true
func scrollViewWillBeginDragging(_: UIScrollView) {
isDragging = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// If not dragging, we could make an early exit
guard isDragging else {
return
}
let topOffset = scrollView.contentOffset.y + statusBarHeight
// If The dismissal has not already started and the user has scrolled to the top and they are currently scrolling, then initiate the interactive dismissal
if !isDismissing && topOffset <= 0 && scrollView.isTracking {
startInteractiveTransition()
return
}
// If its already being dismissed, then calculate the progress and update the interactive dismissal animator
if isDismissing {
updateInteractiveTransitionProgress()
}
}
// Once the scroll ends, check for a few things
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Early return
if !isDismissing {
return
}
// Optional check to dismiss the controller, if swiped from the top
checkForFastSwipes()
// If dragged enough, dismiss the controller, otherwise cancel the
transition
if interactor?.shouldFinish ?? false {
interactor?.finish()
} else {
interactor?.cancel()
}
// Finally reset the transition properties
resetTransitionProperties()
}
private func checkForFastSwipes() {
let velocity = scrollView.panGestureRecognizer.velocity(in: view)
let velDiff = velocity.y - velocity.x
if velDiff > 0 && velDiff >= 75 {
interactor?.hasStarted = false
self.dismiss()
interactor?.shouldFinish = true
}
}
private func startInteractiveTransition() {
isDismissing = true
interactor?.hasStarted = true
dismiss()
}
private func updateInteractiveTransitionProgress() {
progress = max(
0.0,
min(1.0, ((-scrollView.contentOffset.y) - statusBarHeight) / 90.0)
)
interactor?.shouldFinish = progress > 0.5
interactor?.update(progress)
}
private func resetTransitionProperties() {
isDismissing = false
isDragging = false
}
The interactor property used for synchronizing the animation with gesture
var interactor: Interactor?
class Interactor: UIPercentDrivenInteractiveTransition {
var hasStarted = false
var shouldFinish = false
}
Inspired by the following Kodeco tutorial
https://www.kodeco.com/books/ios-animations-by-tutorials/v6.0/chapters/25-uiviewpropertyanimator-view-controller-transitions
(Look for the Interactive view controller transitions section)
Edit
After implementing the solution below, I realized that it only works if you have sufficient content to scroll, however, if you have dynamic content wherein the contents are not guaranteed to be scrollable as was my case, you'd be better off adding a pan gesture as mentioned by #trungduc. However, there are a few improvements that we could make to their answer like detecting an upwards scroll and not letting it interfere with our gesture.
Under the changed state add the following code
let isUpwardsScroll = self.velocity(in: target).y < 0
/*
If the user is normally scrolling the view, ignore it. However,
once the interaction starts allow such gestures as they could be
dragging the interactable view back
*/
if isUpwardScroll && !interactor.hasStarted {
return
}
In modern user interfaces on iOS, it is often useful to implement multiple UIGestureRecognizers on a single view, in order to provide more realistic behavior of displayed objects that model the real world.
For example, you might want to be able to both drag a view around the screen, but also use two fingers to rotate it.
The UIGestureRecognizerDelegate provides an optional function shouldRecognizeSimultaneouslyWith for this purpose. Returning true avoids only one gesture having effect at a time:
// MARK: - UIGestureRecognizerDelegate
extension GestureController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
However, when multiple gesture recognisers are active, especially UIRotationGestureRecognizer it can be frustrating to find the view behaving unexpectedly as the handlers constantly override each other.
How can multiple gesture recognisers be implemented to provide smooth behavior?
The key to implementing multiple gesture recognisers simultaneously is modifying their CGAffineTransforms rather than overwriting them.
Apple > Documentation > Core Graphics > CGAffineTransform:
Note that you do not typically need to create affine transforms directly. If you want only to draw an object that is scaled or rotated, for example, it is not necessary to construct an affine transform to do so. The most direct way to manipulate your drawing—whether by movement, scaling, or rotation—is to call the functions
translateBy(x:y:)
,
scaleBy(x:y:)
, or
rotate(by:)
, respectively. You should generally only create an affine transform if you want to reuse it later.
Furthermore, when changes are detected, after applying the translation, it is important to reset the value of the sender, so that the translations do not compound each time they are detected.
For example:
#IBAction func rotateAction(_ sender: UIRotationGestureRecognizer) {
guard let view = sender.view else { return }
switch sender.state {
case .changed:
view.transform = view.transform.rotated(by: sender.rotation)
sender.rotation = 0
default: break
}
}
and
#IBAction func panAction(_ sender: UIPanGestureRecognizer) {
guard let view = sender.view else { return }
switch sender.state {
case .changed:
let translationX = sender.translation(in: view).x
let translationY = sender.translation(in: view).y
view.transform = view.transform.translatedBy(x: translationX, y: translationY)
sender.setTranslation(CGPoint.zero, in: view)
default: break
}
}
I’m trying to create an application which duplicates the ability of Apple’s Photos app (iPhone) to zoom, pan and scroll through photographic images. (I also want to use the same controls when viewing pdfs and other documents.) I got the tap gesture to show/hide the navigation bar and the swipe gesture to scroll through the images from left to right & vice versa. Then I got the pinch gesture to zoom in and out, but when I added the pan gesture to move around within a zoomed image, the swipe gesture quit working.
I found potential solutions elsewhere on StackOverflow including the use of shouldRecognizeSimultaneouslyWithGestureRecognizer, but so far I have not been able to resolve the conflict. Any suggestions?
Here's the code:
func gestureRecognizer(UIPanGestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer UISwipeGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
#IBAction func handlePinch(sender: UIPinchGestureRecognizer) {
sender.view!.transform = CGAffineTransformScale(sender.view!.transform, sender.scale, sender.scale)
sender.scale = 1
}
#IBAction func handlePan(sender: UIPanGestureRecognizer) {
self.view.bringSubviewToFront(sender.view!)
var translation = sender.translationInView(self.view)
sender.view!.center = CGPointMake(sender.view!.center.x + translation.x, sender.view!.center.y + translation.y)
sender.setTranslation(CGPointZero, inView: self.view)
}
#IBAction func handleSwipeRight(sender: UISwipeGestureRecognizer) {
if (self.index == 0) {
self.index = ((photos.count) - 1);
}
else
{
self.index--;
}
// requireGestureRecognizerToFail(panGesture)
setImage()
}
You do not want shouldRecognizeSimultaneouslyWithGestureRecognizer: (which allows two gestures to happen simultaneously). That's useful if you want to, for example, simultaneously pinch and pan. But the simultaneous gestures will not help in this scenario where you are panning and swiping at the same time. (If anything, recognizing those simultaneously probably confuses the situation.)
Instead, you might want to establish precedence of swipe and pan gestures (e.g. only pan if swipe fails) with requireGestureRecognizerToFail:.
Or better, retire the swipe gesture entirely and use solely the pan gesture, which, if you're zoomed out will be an interactive gesture to navigate from one image to the next, and if zoomed in, pans the image. Interactive pan gestures generally a more satisfying UX, anyway; e.g., if swiping from one photo to the next, be able to stop mid pan gesture and go back. If you look at the Photos.app, it's actually using a pan gesture to swipe from one image to another, not a swipe gesture.
I discovered a tutorial at http://www.raywenderlich.com/76436/use-uiscrollview-scroll-zoom-content-swift that does a great job of introducing UIScrollView as a way of combining zooming, panning and paging in Swift. I recommend it for anyone trying to learn how to make these gestures work well together.
In similar case I've used another approach: extended pan gesture to support swipe:
// in handlePan()
switch recognizer.state {
struct Holder {
static var lastTranslate : CGFloat = 0
static var prevTranslate : CGFloat = 0
static var lastTime : TimeInterval = 0
static var prevTime : TimeInterval = 0
}
case .began:
Holder.lastTime = Date.timeIntervalSinceReferenceDate
Holder.lastTranslate = translation.y
Holder.prevTime = Holder.lastTime
Holder.prevTranslate = Holder.lastTranslate
//perform appropriate pan action
case .changed:
Holder.prevTime = Holder.lastTime
Holder.prevTranslate = Holder.lastTranslate
Holder.lastTime = Date.timeIntervalSinceReferenceDate
Holder.lastTranslate = translation.y
//perform appropriate pan action
case .ended ,.cancelled:
let seconds = CGFloat(Date.timeIntervalSinceReferenceDate) - CGFloat(Holder.prevTime)
var swipeVelocity : CGFloat = 0
if seconds > 0 {
swipeVelocity = (translation.y - Holder.prevTranslate)/seconds
}
var shouldSwipe : Bool = false
if Swift.abs(swipeVelocity) > velocityThreshold {
shouldSwipe = swipeVelocity < 0
}
if shouldSwipe {
// perform swipe action
} else {
// perform appropriate pan action
}
default:
print("Unsupported")
}
All you need to do is to find appropriate velocityTreshold for your swipe gesture