I am using XWalkView which is a subclass of WKWebView, added as a subview to a container view programmatically. I am trying to zoom the content page of the WKWebView but it does not zoom first time. The WKWebView zooms the content page subsequently, i.e. when I pinch zoom or do double tap zoom it zooms WKWebView itself or container view (my guess) & after that, doing zoom gestures again the content page is zoomed (which is expected behaviour).
The question is that how to make the content page of WKWebView zoom whenever the zoom gesture is performed.
I am conforming to UIScrollViewDelegate protocol in my ViewController.h as below:
#interface ViewController : UIViewController<UIScrollViewDelegate, WKNavigationDelegate, WKUIDelegate>
I am setting the delegate to self in viewDidLoad as below:
xWalkView.scrollView.delegate = self;
Setting zooming scales in viewDidLoad as below:
xWalkView.scrollView.minimumZoomScale = 1;
xWalkView.scrollView.maximumZoomScale = 6;
& implementing the protocol methods in ViewController.m as below:
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
if (shouldPageNotZoom) {
NSLog(#"viewForZoomingInScrollView YES");
return nil;
} else {
NSLog(#"viewForZoomingInScrollView NO");
return xWalkView.scrollView.subviews[0];
}
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
}
I am using a BOOL shouldPageNotZoom & setting it YES or NO based on URL depending on whether zooming is required or not. On pages where zooming is required I can see the log of else block (NSLog(#"viewForZoomingInScrollView NO");) being printed in log area & on pages where zooming is not required I can see the log of if block (NSLog(#"viewForZoomingInScrollView YES");). Which is working. The only problem is content page does not zoom on first zooming gesture instead WKWebView or container view is zoomed (I guess), but zooms on subsequent zoom gestures.
After some observation I found that subview at index 0 of scrollview is being zoomed the first & subsequent times (but the content page zooms subsequent times not the first time), which is being returned in viewForZoomingInScrollView. Which should be the case, & is. I did this by assigning the tag values to each view & seeing the tag value by logging the tag value in scrollViewDidEndZooming method in log area as below:
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
if (shouldPageNotZoom) {
NSLog(#"viewForZoomingInScrollView YES");
return nil;
} else {
NSLog(#"viewForZoomingInScrollView NO");
xWalkView.scrollView.tag = 20;
xWalkView.tag = 15;
xWalkView.scrollView.subviews[0].tag = 10;
_containerView.tag = 5;
return xWalkView.scrollView.subviews[0];
}
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
NSLog(#"tag %li", (long) view.tag);
}
Giving the below log:
2017-11-28 17:21:36.610753+0530 MyApp[4614:241659] tag 10 //Getting this 1st time of zooming
2017-11-28 17:21:59.369461+0530 MyApp[4614:241659] tag 10 //Getting this at subsequent zooms
I don't know what am I missing.
Please comment if any clarification is needed.
Got the solution! Returning the last subview first time & first subview of scrollview on subsequent zooms in viewForZoomingInScrollView method solved the problem, as shown below:
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
if (shouldPageNotZoom) {
NSLog(#"viewForZoomingInScrollView YES");
return nil;
} else {
NSLog(#"viewForZoomingInScrollView NO");
xWalkView.scrollView.tag = 20;
xWalkView.tag = 15;
xWalkView.scrollView.subviews[0].tag = 10;
_containerView.tag = 5;
if (isFirstTimeZooming) {
return xWalkView.scrollView.subviews[xWalkView.scrollView.subviews.count - 1];
} else {
return xWalkView.scrollView.subviews[0];
}
}
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
NSLog(#"tag %li", (long) view.tag);
isFirstTimeZooming = NO;
}
isFirstTimeZooming is set to YES on load of the page in didStartProvisionalNavigation method & set to NO in scrollViewDidEndZooming method so that first subview of scrollview can be passed on subsequent zooming.
I had to use isFirstTimeZooming because if I pass last subview only in viewForZoomingInScrollView the zooming problem was reversed, i.e. it was zooming properly first time but not on subsequent zooms.
Related
I have two UIScrollViews. Both the scroll views contain an image. Now, I want to zoom second scroll view programmatically if I zoom in the first scroll view manually. I am using -(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView method to manually zoom the first scroll view. The second scroll view should be zoomed to the same point and scale. How can I achieve this?
By using the tag or object of those two scrollviews you can identify the which scroll view you need zoom in viewForZoomingInScrollView method.
e.g
//need to set the tag for scrollviews
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
if(scrollView.tag == 1)
{
return firstImageView;
}
return secondImageView;
}
OR
//need to create the object of used scrollviews
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
if(scrollView == objScrollViewFirst)
{
return firstImageView;
}
return secondImageView;
}
How about something like this:
CGRect zoomRect;
zoomRect.size.height = manualScrollView.frame.size.height / manualScrollView.zoomScale;
zoomRect.size.width = manualScrollView.frame.size.width / manualScrollView.zoomScale;
zoomRect.origin.x = manualScrollView.center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = manualScrollView.center.y - (zoomRect.size.height / 2.0);
[autoScrollView zoomToRect:zoomRect animated:YES];
[autoScrollView setZoomScale:manualScrollView.zoomScale animated:YES];
Try something like this in the UIScrollViewDelegate method:
override func scrollViewDidScroll(scrollView: UIScrollView){
otherScrollView.contentOffset = scrollView.contentOffset
otherScrollView.zoomScale = scrollView.zoomScale
}
iOS7 & iOS8
I need to disable 2 or three fingers scrolling in UIScrollview.
I tried :
[self.scrollView.panGestureRecognizer setMaximumNumberOfTouches:1];
[self.scrollView.panGestureRecognizer setMinimumNumberOfTouches:1];
But it has no effect. It is still possible to scroll with 2 fingers.
If i tried to set max and min to 2. One finger scrolling was disabled but 3 fingers scrolling possible :(
I tried this too, but without success:
for (UIGestureRecognizer* pan in self.scrollView.gestureRecognizers) {
OTTNSLog(#"touches: %ld", (unsigned long)pan.numberOfTouches);
if ([pan isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *mpanGR = (UIPanGestureRecognizer *) pan;
mpanGR.minimumNumberOfTouches = 1;
mpanGR.maximumNumberOfTouches = 1;
}
if ([pan isKindOfClass:[UISwipeGestureRecognizer class]])
{
UISwipeGestureRecognizer *mswipeGR = (UISwipeGestureRecognizer *) pan;
mswipeGR.numberOfTouchesRequired = 1;
}
}
Does anybody know, how it solve this ?
Thanks.
PROBLEM:
When the UIPanGestureRecognizer is underlying a UIScrollView - which unfortunately does also effect UIPageViewController - the maximumNumberOfTouches is not behaving as expected, the minimumNumberOfTouches however always limits the lower end correctly.
When monitoring these parameters they report back correct values - they seem to do their job - it's just that UIScrollView itself doesn't honor them and ignores their settings!
SOLUTION:
Set the minimumNumberOfTouches to the desired value e.g. 1 and - very importantly - the maximumNumberOfTouches to 2 !!!
myScrollView.panGestureRecognizer.minimumNumberOfTouches = 1;
myScrollView.panGestureRecognizer.maximumNumberOfTouches = 2;
Conform to the UIGestureRecognizerDelegate protocol in your scrollView's #interface declaration. You don't have to set the panGestureRecognizer.delegate for a UIScrollView!!! The delegate is already set because UIScrollView requires to be the delegate of its own pan/pinchGestureRecognizer.
Then implement the UIGestureRecognizer delegate method:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%d", gestureRecognizer.numberOfTouches);
NSLog(#"%#", gestureRecognizer.description);
if (gestureRecognizer == self.panGestureRecognizer) {
if (gestureRecognizer.numberOfTouches > 1) {
return NO;
} else {
return YES;
}
} else {
return YES;
}
}
}
AN EVEN SAFER VERSION:
If you have a custom scrollView class and wanna be on the VERY safe side you can also add one more line of code to disambiguate against other scrollViews:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%d", gestureRecognizer.numberOfTouches);
NSLog(#"%#", gestureRecognizer.description);
if ([gestureRecognizer.view isMemberOfClass:[MY_CustomcrollView class]]) {
if (gestureRecognizer == self.panGestureRecognizer) {
if (gestureRecognizer.numberOfTouches > 1) {
return NO;
} else {
return YES;
}
} else {
return YES;
}
} else {
return YES;
}
}
ADDITIONAL INFORMATION:
The NSLogs tell you the number of touches. If you set both the min and max to the same value (like 1 in the example above) the if-loop would never be triggered... ;-)
That is why maximumNumberOfTouches has to be at least minimumNumberOfTouches + 1
panGestureRecognizer.minimumNumberOfTouches = 1;
panGestureRecognizer.maximumNumberOfTouches = minimumNumberOfTouches + 1;
GEEK SECTION:
for (UIView *view in self.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
NSLog(#"myPageViewController - SCROLLVIEW GESTURE RECOGNIZERS: %#", view.gestureRecognizers.description);
((UIPanGestureRecognizer *)view.gestureRecognizers[1]).minimumNumberOfTouches = 1;
((UIPanGestureRecognizer *)view.gestureRecognizers[1]).maximumNumberOfTouches = 2;
}
}
This is the way to access the underlying scrollView that is responsible for the paging of a UIPageViewController. Put this code in the e.g. viewDidLoad: of the UIPageViewController (self).
If you don't have access to the scrollView at all - like for a UIPageViewController in a dynamic UITableViewCell where creation and cell reuse happens at runtime and no outlets can be set on its contentViews - put a category on UIScrollView and override the delegate method there. But be careful! This effects every scrollView in your application - so do proper introspection (class-checking) like in my 'EVEN SAFER' example above... ;-)
SIDENOTE:
Unfortunately the same trick doesn't work with the pinchGestureRecognizer on UIScrollView because it doesn't expose a min/maxNumberOfTouches property. When monitored it always reports 2 touches (which you obviously need to pinch) - so its internal min/maxNumberOfTouches seem to have been set both to 2 - even if UIScrollView isn't honoring its own settings and keeps happily pinching with any numbers of fingers (more than 2). So there is no way to restrict pinching to a limited amount of fingers...
I can confirm this is still an issue in iOS 8, but only when the UIPanGestureRecognizer is underlying a UIScrollView. Creating a UIView with a fresh UIPanGestureRecognizer and setting its maximumNumberOfTouches property works as expected.
Interesting note: if you query the UIScrollView's UIPanGestureRecognizer while you're scrolling, it reports the number of touches as less than or equal to the maximum. In other words, if you set the maximum to 2, and scroll with 3 fingers, it reports the gesture as a 2-finger scroll. Subsequently letting up fingers one at a time while continuing to scroll usually (but not consistently) reduces the reported number of touches as well — so if you go from 3 -> 2 -> 1 fingers, it will register 2 -> 1 -> 0 touches, and stop scrolling while you still have 1 finger on the device.
Submitted rdar://20890684 and copied to http://openradar.appspot.com/radar?id=6191825677189120. Please feel free to dupe.
There is no specific method available for this just do some tricks but results are only 75%.
Add swipe gestures(4 directions each) and double tap gesture to your UIScrollview..Then use below code..
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeDown];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeLeft];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeRight];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeUp];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:doubleTap];
This will do the job for you:
yourscrollView.multipleTouchEnabled=NO;
I want to achieve something very close to what Google Maps (iOS) does and I have some doubts.
But first, a better explanation and things to take into account:
-------------- ANSWER --------------
Thanks to Jugale's contribution, here's a repository so everybody can download and test everything out.
https://github.com/vCrespoP/VCSlidingView
-------------- ORIGINAL QUESTION -----------
You tap in a point inside the map, a view comes in from the bottom but then when you interact with that summary view:
Notice when pulling just a bit, the navigation bar already has set.
When you have scrolled it to the top, you can continue scrolling and the inner scrollview will continue scrolling.
When you 'reverse' the action to dismiss the view, the PanGesture doesn't mess up with the inner scrollView (same for the other way, scrollView VS dismiss)
Here it is in action:
Doubts:
I've tried to do it as an interactive transition (UIPercentDrivenInteractiveTransition) and separating Map from Details in 2 controllers but I'm having troubles with the UIPanGesture interfering with the scrollView.
Maybe it's better to do it as a subview and handle everything there? More or less like MBPullDownController (Although it has some issues with iOS8) -> https://github.com/matej/MBPullDownController
So, anybody knows any framework, has done it, or knows how to do this in a good way?
Thank you for your time :D
Looking through my implementation it seems the following are true:
I have a subclass of UIViewController that is the view controller
I have a subclass of UIView that is the overlay (and henceforth with the known as "the overlay") (actually for me this is a UIScrollView because it needs to go sideways too, but I'll try and filter out the unnecessary code)
I have another subclass of UIView that loads the overlay's content ("the content wrapper")
The content wrapper has a UIScrollView property, in which all other views are loaded ("the content view")
The view controller is responsible for initializing the overlay, setting it's initial frame (where the height is the height of the screen) and passing content to it, nothing more.
From it's -initWithFrame method, the overlay sets itself up with a UIDynamicItemBehavior. It also creates some UICollisionBehavior objects: one at the top of the screen and one below the bottom of the screen at just the right y position for the top of the overlay to be partially visible (as seen in the first frame of your GIF). A UIGravityBehavior is also set up to keep the overlay sitting on the lower collision boundary. Of course, _animator = [[UIDynamicAnimator alloc... is set up too.
Finally:
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan)];
_pan.delegate = self;
_pan.cancelsTouchesInView = FALSE;
The overlay class also has some other helpful methods such as changing the gravity's direction so that the overlay can appear to snap to the top or bottom of the screen.
The _pan handler uses a UISnapBehavior to keep the overlay moving dynamically up and down the screen underneath the user's finger:
- (void)handlePan
{
[self handlePanFromPanGestureRecogniser:_pan];
}
- (void)handlePanFromPanGestureRecogniser:(UIPanGestureRecognizer *)pan
{
CGFloat d = [pan velocityInView:self.superview.superview].y;
CGRect r = self.frame;
r.origin.y = r.origin.y + (d*0.057);
if (r.origin.y < 20)
{
r.origin.y = 20;
}
else if (r.origin.y > [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT)
{
r.origin.y = [UIScreen mainScreen].bounds.size.height - PEEKING_HEIGHT;
}
if (pan.state == UIGestureRecognizerStateEnded)
{
[self panGestureEnded];
}
else if (pan.state == UIGestureRecognizerStateBegan)
{
[self snapToBottom];
[self removeGestureRecognizer:_tap];
}
else
{
[_animator removeBehavior:_findersnap];
_findersnap = [[UISnapBehavior alloc] initWithItem:self snapToPoint:CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r))];
[_animator addBehavior:_findersnap];
}
}
- (void)panGestureEnded
{
[_animator removeBehavior:_findersnap];
CGPoint vel = [_dynamicSelf linearVelocityForItem:self];
if (fabsf(vel.y) > 250.0)
{
if (vel.y < 0)
{
[self snapToTop];
}
else
{
[self snapToBottom];
}
}
else
{
if (self.frame.origin.y > (self.superview.bounds.size.height/2))
{
[self snapToBottom];
}
else
{
[self snapToTop];
}
}
}
The content wrapper listens for scroll events generated by the content view:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//this is our fancy way of getting the pan to work when the scrollview is in the way
if (scrollView.contentOffset.y <= 0 && _dragging)
{
_shouldForwardScrollEvents = TRUE;
}
if (_shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapper:isForwardingGestureRecogniserTouches:)])
{
[_delegate theContentWrapper:self isForwardingGestureRecogniserTouches:scrollView.panGestureRecognizer];
}
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
_dragging = FALSE;//scrollviewdidscroll must not be called after this
if (scrollView.contentOffset.y <= 0 || _shouldForwardScrollEvents)
{
if ([_delegate respondsToSelector:#selector(theContentWrapperStoppedBeingDragged:)])
{
[_delegate theContentWrapperStoppedBeingDragged:self];
}
}
_shouldForwardScrollEvents = FALSE;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
_dragging = TRUE;
}
As you can see, when the bool shouldForwardScrollEvents is TRUE then we send scrollView.panGestureRecognizer to the content wrapper's delegate (the overlay). The overlay implements the delegate methods like so:
- (void)theContentWrapper:(TheContentWrapper *)contentWrapper isForwardingGestureRecogniserTouches:(UIPanGestureRecognizer *)contentViewPan
{
[self handlePanFromPanGestureRecogniser:contentViewPan];
}
- (void)theContentWrapperStoppedBeingDragged:(TheContentWrapper *)contentWrapper
{
//because the scrollview internal pan doesn't tell use when it's state == ENDED
[self panGestureEnded];
}
Hopefully at least some of this is useful to someone!
I am creating an app where I am trying to have a pagescrollviewcontroller swipe through screens and have the top "nav" bar titles scroll real time with the titles
i have the title bar view as a custom view and I am able to access the scroll delegate methods for both the custom scroll view and the pageview controller. However. I don't see how to access the real time scroll pos? I know it is possible because twitter does it (really can't shouldn't be in anyone's vocabulary) but I am not sure how to achieve this.
a picture
the home title swipes at the same scroll pos as the pagecontrollers.
current code:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
if(scrollView.tag == 100) {
CGPoint point = CGPointMake(mainScrollView.contentOffset.x * 1,0);
[titleSwipe setContentOffset:point animated:YES];
}
else {
mainScrollView.contentOffset = CGPointMake(0,1);
[mainScrollView setContentOffset:titleSwipe.contentOffset animated:YES];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate: (BOOL)decelerate {
CGPoint point = CGPointMake(mainScrollView.contentOffset.x * 2,0);
[titleSwipe setContentOffset:point animated:YES];
}
You'll find the real time position in scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// X scroll
//
CGFloat percentage = scrollView.contentOffset.x / scrollView.contentSize.width;
NSLog(#"Scrolled percentage: %f", percentage);
}
Hope that helps!
If showsHorizontalScrollIndicator is enabled, a small horizontal indicator is displayed at the bottom of the UIScrollView. I was wondering if anyone could tell me how this is calculated. The reason being, I want to display a custom indicator in a horizontal navigation system if there is content left/right, show/hiding when content is available.
I just use scrollView.contentOffset.x. If you want to know what page you are on, you divide by your page width (assuming you have a standard width). So
int currentPage=(int)scrollView.contentOffset.x/SCREEN_WIDTH; for example. Now if you want to display a position indicator, like in the iOS homepage, you can do it in a loop in
-(void)scrollViewDidScroll:(UIScrollView *)sender something like
currentPage=(int)scrollView.contentOffset.x/SCREEN_WIDTH;
for(int i=0; i<pageCount; i++){
if(i==currentPage)//display a filled in 'o'
else //display an empty 'o'
}
Oh, BTW, -(void)scrollViewDidScroll:(UIScrollView *)sender is not called when the UIScrollView is initially loaded, so you perhaps should call it yourself (in order to display the indicator before the user has scrolled). You can also use scrollView.contentOffset.y off course, so you could easily adapt this to show a '+' shaped arrangement of 'o's, hinting at content above and below also.
I used the following solution. Not the prettiest, but it seems to work. Requires UIScrollViewDelegate.
-(void)scrollViewDidScroll:(UIScrollView *)sender
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self scrollViewDidEndScrollingAnimation:subNavigation];
}
-(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (scrollView.contentOffset.x <= 0) {
[self scrollViewReachedLeftBound];
} else if (scrollView.contentOffset.x >= 300) {
[self scrollViewReachedRightBound];
} else {
[leftArrow setHidden:NO];
[rightArrow setHidden:NO];
}
}
- (void)scrollViewReachedLeftBound
{
[leftArrow setHidden:YES];
[rightArrow setHidden:NO];
}
- (void)scrollViewReachedRightBound
{
[leftArrow setHidden:NO];
[rightArrow setHidden:YES];
}