In handling a UIPanGestureRecognizer in iOS, guidance such as that found here
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/handling_uikit_gestures/handling_pan_gestures?language=objc
and
https://material.io/guidelines/patterns/gestures.html#gestures-drag-swipe-or-fling-details
advises using the velocity property to distinguish a normal drag from a swipe or a flick/fling. Nowhere does it say what a typical threshold is. For the sake of example, say we're dragging a thumbnail (44x44 points) across an iOS screen. Fine-tuning aside, above what velocity y-value would you consider the pan gesture to be a flick/fling?
Context: I'm trying to implement the iOS behavior you see in iOS 11 on an iPhone X, where swiping upward on the bar flings an app back to its home icon, except I'm doing it on cells being flung back to a UICollectionView.
After doing some research I found that Apple uses velocity of 300 to detect flicking in ScrollView.
extension TestViewController: UIScrollViewDelegate {
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
print(scrollView.panGestureRecognizer.velocity(in: view)) // if velocity > 300, UIScrollView will scroll to next page
}
}
Related
In UIScrollView, there is a feature named "Keyboard Dismiss interactively"
By using such option, this enables me to implement the following drag down to hide keyboard
However, the keyboard dismiss operation only kick start, when the UIScrollView drag action touches keyboard edge.
What I would like to achieve is, the keyboard dismiss operation kick start, when the UIScrollView drag operation touches the bottom toolbar edge.
What I wish to achieve (Same as WhatsApp)
As you can see from the video, the keyboard dismiss operation will kick start, when the drag operation touches the bottom bar edge, even before touching keyboard edge.
May I know, what technique WhatsApp is using, to achieve such behavior?
Side note
You may notice our bottom toolbar does move along with keyboard. This is because there is a bottom constraint for bottom toolbar's bottom with Safe Area's bottom.
We adjust the bottom constraint's constant value, by installing a gesture recognizer in global Window. This is the code snippet to achieve such technique.
#objc private func didPan(_ sender: UIPanGestureRecognizer){
if keyboardHeight > 0 {
let mainScrollView = editable.mainScrollView
let isScrolling = (mainScrollView.isDragging || mainScrollView.isDecelerating)
if isScrolling {
if let mainScrollViewGlobalOrigin = mainScrollView.globalOrigin {
let point = sender.location(in: sender.view!)
// Take safe area into consideration, like iPhone 12 Pro Max.
let key = UIWindow.key
let bottomSafeArea = key?.safeAreaInsets.bottom ?? 0
let dy = point.y - (
mainScrollViewGlobalOrigin.y +
mainScrollView.frame.height +
toolbarHeightLayoutConstraint.constant +
bottomSafeArea -
bottomLayoutConstraint.constant -
self.keyboardHeight
)
if dy > 0 {
bottomLayoutConstraint.constant = -(keyboardHeight - dy)
}
}
}
}
}
The reason that WhatsApp behaves like this is that their view is considered to be part of the keyboard, so when the swipe gesture reaches their custom view it will begin interactive dismissal.
To achieve this yourself all you need to do is provide the toolbar view as the inputAccessoryView for your view controller. You won't need the constraints for positioning as the keyboard window would then control your toolbar's position.
There is also inputAccessoryViewController for the times where your toolbar may not be a UIView, but instead an entire UIViewController.
The views in either of these properties will only be visible when the keyboard is visible, so to get around that you'll still want to put it into your view hierarchy, but remove/add it based on becoming/resigning first responder.
EDIT: Also, you should be using UIApplication.keyboardDidChangeFrameNotification to detect when the keyboard changes size/position/etc and allow you to adjust insets/positions of views appropriately. In modern iOS there are plenty of ways the keyboard can change size while open, and observing that notification is the correct way to handle the keyboard size.
I have a scroll transition style UIPageViewController that needs to disable paging only when device is in landscape orientation. But paging should be enabled in portrait orientation.
I have encountered similar questions here in SO but not my specific need. Some of them are:
How do I Disable the swipe gesture of UIPageViewController?
Disable Page scrolling in UIPageViewController
Disable/enable scrolling in UIPageViewController
Restrict UIPageViewController (with TransitionStyleScroll) pan gesture to a certain area
All of the above points to either completely disabling or restricting pan gesture to a certain area.
Now if I take the approach of completely disabling:
I will need to track device orientation change
Disable when orientation is set to landscape
Again enable when orientation is changed to portrait
If I take the approach of restricting to a certain area:
I will need to find that certain area
That certain area (described in previous point) needs to be calculated
differently for portrait & landscape orientation
Certain area for portrait orientation needs to be the area of the
whole UIPageViewController bounds
Certain area for landscape orientation needs to be a very minimum area
(whose frame could be 0, 0, 1, 1) where user won't be able to
perform pan operation. This frame calculation needs to be very
precise because my UIPageViewController takes the whole bounds of
the main screen in landscape orientation.
Then again may need to track device orientation change for different
calculation of the certain area
There are some techniques where the authors suggest:
pvc.dataSource = nil // prevents paging
pvc.dataSource = `a valid dataSource object` // enables paging
So, manual enable + disable again. Track orientation change and enable/disable.
This isn't safe to use for my specific use case as there is a possibility of assigning data source multiple times.
There are other approaches which, I think, can't be modified to fit the use case.
Is there a shortcut way to achieve what I need?
Answering to my own question as I've already achieved what I needed to.
Subclassing UIPageViewController is the easiest way. We have to find the underlying UIScrollView that is used by the page view controller to handle its pan gesture related work. We will add another UIPanGestureRecognizer to that internal scroll view. This pan gesture recognizer won't perform any action essentially but it will block the internal pan gesture recognizer to be recognized for landscape orientation only.
Sample implementation:
class CustomPageViewController: UIPageViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
if let underlyingScrollView = view.subviews.compactMap({ $0 as? UIScrollView })
.first {
let pangestureRecognizer = UIPanGestureRecognizer()
pangestureRecognizer.delegate = self
underlyingScrollView.addGestureRecognizer(pangestureRecognizer)
// at this point, the underlying scroll view will have two pan gesture
// recognizer side by side. We have the control of our added pan gesture
// recognizer through the delegate. We can conditionally recognize it or not
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer)
-> Bool {
// Returning true from here means, page view controller will behave as it is
// Returning false means, paging will be blocked
// As I needed to block paging only for landscape orientation, I'm just returning
// if orientation is in portrait or not
return UIApplication.shared.statusBarOrientation.isPortrait
}
}
iMessage has the ability to swipe between apps by panning left or right. This presents new view controller, when swiping far/fast enough. My issue is that while swiping to a new app, the stickers in my collectionView are scrolling up and down if I move my finger vertically at all. I want the stickers to stay in place as I am panning from one app to the next.
Here is an example of what I mean:
(click the image to see gif...)
Here is what I tried that didn't quite work for me:
//using the scrollViewDelegate
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x != 0 {
scrollView.isScrollEnabled = false
}
}
^ this would work, but disables scrolling until the view is loaded again or the user drags left or right. So not at all a solution.
I use a ImageScrollView from here, which is basically a UIScrollView to allow pinch to zoom into a picture. I now wanted to add the possibility to swipe down the picture to dismiss the view controller. I created a UIPanGestureRecognizer and it works fine if zoom scale is at the minimum value (so the whole picture is visible without zoom). But how can I skip the pan gesture recognizer if the zoom scale is above the minimum value? Because it lays on top of the ImageScrollView, I can't scroll in the picture because scrolling gesture is fetched by the UIPanGestureRecognizer. Any idea how to solve this?
For show image like whatsApp imageView functionality you go with the apple framework QuikLook. It will automatically handle Zoom, Dismiss the image while Swipe etc.
It also support for the documents. It will reduce your effort a lot
It's too easy to handle this operation by adding a trigger on swipe action (gesture calling method).
When zoomScale > minimumValue; set returnstatement
A simple example with Swift 4:
let zoomScale: 1.0
let minimumValue: 0.5
func handlePanGesture(gesture: UIPanGestureRecognizer) {
if (#<set pan gesture down moving condition>#) {
if (zoomScale > minumumValue) {
return
}
}
// perform your next operations
}
I'm building an iOS application that requires the same effect seen when swiping back and forth in Safari.
When swiping to go back, the foreground panel moves out of the way but the panel in the back is moving a bit as well. Very similar to the horizontal scrolling that exists in the Yahoo Weather app.
Is this a built-in control with iOS 7? I'm seeing it in a lot of places but can't quite figure out how to do it.
I think there are 2 UIScrollView working together, when the user scrolls on the foreground UIScrollView, you should move the background UIScrollView, something like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if ([scrollView isEqual:self.scrollViewForeground]) {
CGPoint offset = self.scrollViewForeground.contentOffset;
offset.x = offset.x * 0.5;
[self.scrollViewBackground setContentOffset:offset];
}
}