I've seen a similar question here but with no working solution.
I have a UIScrollView that is embedded in a UIPageViewController. I would like to have the user able to scroll the UIScrollView normally. Then, if one end of the UIScrollView is reached (all the way to the left, for example) then swiping left (swiping right, but you get what I mean) would trigger the UIPageViewController to shift left to another page.
The default behavior is a bit sketchy. It seems like the gesture recognizers compete and in some cases the UIScrollView does scroll, and in other cases the PageView just shifts to the next page. I'd like to be able to tie this behavior to the content offset of the UIScrollView, or something.
I'll be trying some experiments with the requireFail methods on the various gesture recognizers, but it seems like all of the PageView's gesture recognizer properties are nil'd out because I'm using the scroll transition type. So...walking the hierarchy?
Anyway, I'd appreciate it if anyone knows an easy solution to this.
The way I ended up solving this was by using a replacement for UIPageViewController: EMPageViewController. Because I had direct access to the UIScrollView, I was able to subclass it.
The solution basically came in subclassing and adding toggle flags to the UIScrollViews (the one FOR the EMPageViewController and the one INSIDE it) to control when they should be active or not, based on various factors (such as the content offset of one of the scroll views). Then I was able to override gestureRecognizerShouldBegin:gestureRecognizer in different ways in the UIScrollView subclasses, checking the toggle flags and the velocity of the panning gesture recognizer.
Something like this (code out of context):
import UIKit
class CustomScrollView: UIScrollView {
// these toggles get set by some senior ViewController
var lockDown = false
var lockUp = false
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
print("= gesture starting")
if let panner = gestureRecognizer as? UIPanGestureRecognizer {
if panner.velocityInView(self).y < 0.0 {
print("== pulling down, lockDown: \(lockDown)")
if lockDown {
print("=== cancelling paging scroll")
return false
}
}
if panner.velocityInView(self).y > 0.0 {
print("== pulling up, lockUp: \(lockUp)")
if lockUp {
print("=== cancelling paging scroll")
return false
}
}
}
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
}
Anyway, I did get it all working slickly in the end. Everyone would probably have a slightly different use case, but the bottom line is that you need to subclass UIScrollView and so UIPageViewController is not an option.
Related
I have observed MKMapView zoom-out on Double tap gesture, I didn't find any way to disable it. I tried to add own Double Tap Gesture to catch Double tap action but it still zoom out. Any thought?
There is no API to disable or change double-tap behavior with MKMapView.
But, if you really want to do so, one approach would be to find and remove the double-tap gesture recognizer from the MKMapView object.
In the project you shared, you could do that in makeUIView in your UIMapView class:
func makeUIView(context: UIViewRepresentableContext<UIMapView>) -> UIViewType {
self.configureView(mapView, context: context)
setRegion(to: palce)
if let v = mapView.subviews.first,
let ga1 = v.gestureRecognizers
{
let ga2: [UITapGestureRecognizer] = ga1.compactMap { $0 as? UITapGestureRecognizer } .filter { ($0.numberOfTapsRequired == 2) }
for g in ga2 {
v.removeGestureRecognizer(g)
}
}
return mapView
}
I wouldn't necessarily suggest that you do so, however.
Apple may change the MKMapView object in the future, which could then break this.
User's tend to prefer that common UI elements behave in expected ways.
Personally, I get rather annoyed when using an app and the developer has changed standard functionality of UI elements. For example, if I see a table view row with a disclosure indicator (the right-arrow / chevron), I expect that tapping the row will "push" to another screen related to that row. I've seen apps that do not follow that pattern, and it just gets confusing.
I've been trying to scroll UICollectionView with horizontal scroll, to the next page when isPagingEnabled property was set as true. I've been working on it for couple of days and I've made a lot of research, but I couldn't find any case like mine. If you already had this problem and if you already found a solution for it, it would be great sharing your solution way with me. Here is my current case;
func sampleTest() {
let collectionView = app.collectionViews[.sampleCollectionView]
collectionView.waitUntil(.exists)
let totalPageCount = collectionView.cells.count
guard totalPageCount > 0 else {
XCTFail("No pages could find in collection to take snapshot.")
return
}
for currentPage in 1...totalPageCount {
snapshot("Page\(currentPage)")
collectionView.swipeLeft()
}
}
Here, swipeLeft() method of XCUIElement is not working as expected in my case. When I call the method, it is not moving to the next page. It swipes a little bit and turn back due to isPagingEnabled = true statement.
In addition, there is another problem that collectionView.cells.count is calculated wrong. It always returns 1. I assume that the reason of the problem is about reusability. Because the other cells has not dequeued yet. Or collectionView.cells.count is not working as I guess?
I have a class that subclasses UIPageViewController which contains 4 view controllers.
I am trying to figure out why the below code randomly stops the UIPageViewController from scrolling as I try to stop the bounce effect on the first and on the last controller on the UIPageViewController?
If you keep track of your currentIndex then the below should be sufficient but its a little buggy because there is a random scenario where it stops scrolling altogether and gets stuck on a random view controller.
I think the scrollView.bounces is a little buggy, perhaps I am missing something because most of the time it works fine, if anyone is able to have a solution based on the below it would be great please.
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = currentIndex == 0 ||
currentIndex == controllers.count - 1
? false
: true
}
I managed to get this issue working but the problem I have is trying to figure out how I can scroll half way into the next controller.
UIPageViewController - Detect scrolling halfway into the next view controller to change button color?
I have a view controller in which a user can move around UIButton, UISlider, and a custom UIView based control by panning the control around the screen. This view controller is used to create custom layout of control by the user. This is all done by adding PanGestureRecognizer to the UIControl to move the position of the control relative to user's finger location.
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(pan))
panRecognizer.delegate = self;
myslider.addGestureRecognizer(panRecognizer)
~~~~~~~~~~~~
//pan handler method
#objc func pan(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: view)
guard let gestureView = gestureRecognizer.view else {
return
}
//Move the center to user's finger location
gestureView.center = CGPoint(x: gestureView.center.x + translation.x, y: gestureView.center.y + translation.y);
gestureRecognizer.setTranslation(.zero, in: view);
if (gestureRecognizer.state == .ended) {
//Save the new location inside data. Not relevant here.
}
}
This worked fine in iOS 13 and below for all the control i mentioned above (with the UISlider being a bit glitchy but it still responded to the pan gesture and i don't need the value of the uislider anyway so it's safe to ignore). However testing the app in iOS 14 reveals that UISlider completely ignore the PanGesture (proven by adding breakpoint that never got called inside the pan gesture handling method).
I have looked at apple's documentation regarding UISlider and found no change at all related to gesture handling so this must be done deliberately in deeper lever. My question is: is there any way to "force" my custom gesture to be executed instead of UISlider's gesture without the need to create transparent button overlay (which i don't know will work or not) / creating dummy slider just for this ?
Additionally i also added UILongPressGestureRecognizer and UITapGestureRecognizer to the control. Which worked fine on other UIButton but completely ignored by the UISlider in iOS14 (in iOS 13 everything worked fine).
Thanks.
Okay.. I found the answer myself after some more digging and getting cue from "claude31" about making a new clean project and testing from there.
The problem was with the overriden beginTracking function. This UISlider of mine is actually subclassed into a custom class and in there the beginTracking function is overriden, as per code below:
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let percent = Float(touch.location(in: self).x / bounds.size.width)
let delta = percent * (maximumValue - minimumValue)
let newValue = minimumValue + delta
self.setValue(newValue, animated: false)
super.sendActions(for: UIControl.Event.valueChanged)
return true
}
This is to make the slider move immediately to the user finger location if the touch is inside the boundaries of the UISlider (without the need to first touch the thumbTrack and sliding it to the position the user wants).
In iOS 13 this function does not block the gestureRecognizer from getting recognized. However in iOS 14B4, overriding this function, with sendActions(for:) method inside it cause the added gestureRecognizer to be ignored completely, be it pan gesture, long press gesture, or even tap gesture.
For me, the solution is to simply add a state to check whether the pan gesture is required in this view controller or not. Because, luckily, i only need the added gestureRecognizer in view controller that does not require the beginTouch function to be customized and vice versa.
Edit:
Originally i wrote that the cause of the problem is due to always true return value, however, I just reread the documentation and the default return value for this function is also true. So i think the root causes of this problem is the sendActions(for:) method, causing the added gestureRecognizer to be ignored. My Answer above has been edited to reflect this.
I have added a UI Button inside of a stack view which is inside of a table view in my storyboard. When I click on my button the correct output is printed in my debugger console but there is no indication in the app that the button has been clicked (no default animation). I have tried looking at my view hierarchy and changing all of the parent views to clip to bounds. Any idea why the button is functioning but not being animated to the user?
The quick fix to your problem is to set delaysContentTouches = false for your table view.
According to the Apple Docs,
If the value of this property is true, the scroll view delays handling the touch-down gesture until it can determine if scrolling is the intent. If the value is false, the scroll view immediately calls touchesShouldBegin(_:with:in:). The default value is true.
See the class description for a fuller discussion.
Alternatively, if you have subclassed the UIScrollView, you can get the same thing done by overriding the following function,
class MyScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return type(of: view) == UIButton.self
}
}