Continuous vertical scrolling between UICollectionView nested in UIScrollView - ios

While I know nested scrollViews aren't ideal, our designers provided me with this setup, so I'm doing my best to make it work. Let's begin!
View Hierarchy
UIView
UIScrollView (Vertical Scrolling Only)
UIImageView
UICollectionView #1 (Horizontal Scrolling Only)
UIImageView (different from previous UIImageView)
UICollectionView #2 (Vertical Scrolling Only)
Important Note
All my views are defined using programmatic Auto Layout. Each successive view in the UIScrollView's subview hierarchy has a y-coordinate dependency on the view that came before it.
The Problem
For the sake of simplicity, let's modify the nomenclature a bit:
_outerScrollView will refer to UIScrollView
_innerScrollView will refer to UICollectionView #2
I'd like for my _outerScrollView to route its touch event to the _innerScrollView upon reaching the bottom of its contentSize. I'd like the reverse to happen when I scroll back up.
At present, I have the following code:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat bottomEdge = [scrollView contentOffset].y + CGRectGetHeight(scrollView.frame);
if (bottomEdge >= [_outerScrollView contentSize].height) {
_outerScrollView.scrollEnabled = NO;
_innerScrollView.scrollEnabled = YES;
} else {
_outerScrollView.scrollEnabled = YES;
_innerScrollView.scrollEnabled = NO;
}
}
where the initial conditions (before any scrolling occurs) is set to:
outerScrollView.scrollEnabled = YES;
innerScrollView.scrollEnabled = NO;
What happens?
Upon touching the view, the outerScrollView scrolls until its bottom edge, and then has a rubber band effect due to _outerScrollView.bounces = YES; If I touch the view again, the innerScrollView scroll until it hits its bottom edge. On the way back up, the same rubber banding effect occurs in the reverse order. What I want to happen is have a fluid motion between the two subviews.
Obviously, this is due to the scrollEnabled conditions that are set in the conditional in the code snippet. What I'm trying to figure out is how to route the speed/velocity of one scrollView to the next scrollView upon hitting an edge.
Any assistance in this issue would be greatly appreciated.
Other Notes
This did not work for me: https://github.com/ole/OLEContainerScrollView
I am considering putting everything in the UIScrollView hierarchy (except for UICollectionView #2) inside UICollectionView #2 supplementaryView. Not sure if that would work.

Figured it out!
First:
_scrollView.pagingEnabled = YES;
Second:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == _scrollView || scrollView == _offersCollectionView) {
CGFloat offersCollectionViewPosition = _offersCollectionView.contentOffset.y;
CGFloat scrollViewBottomEdge = [scrollView contentOffset].y + CGRectGetHeight(scrollView.frame);
if (scrollViewBottomEdge >= [_scrollView contentSize].height) {
_scrollView.scrollEnabled = NO;
_offersCollectionView.scrollEnabled = YES;
} else if (offersCollectionViewPosition <= 0.0f && [_offersCollectionView isScrollEnabled]) {
[_scrollView scrollRectToVisible:[_scrollView frame] animated:YES];
_scrollView.scrollEnabled = YES;
_offersCollectionView.scrollEnabled = NO;
}
}
}
Where:
_scrollView is the _outerScrollView
_offersCollectionView is the _innerScrollView (which was UICollectionView #2 in my original post).
Here's what happens now:
When I swipe up (so the view moves down), the offersCollectionView takes over the entire view, moving the other subViews out of the view.
If I swipe down (so the views up), the rest of the subviews come back into focus with the scrollView's bounce effect.

Accepted answer didn't work for me. Here's what did:
Define a subclass of UIScrollView:
class CollaborativeScrollView: UIScrollView, UIGestureRecognizerDelegate {
var lastContentOffset: CGPoint = CGPoint(x: 0, y: 0)
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return otherGestureRecognizer.view is CollaborativeScrollView
}
}
Since it's not possible to reroute touches to another view, the only way to ensure that the outer scrollview can continue scrolling once the inner one stops is if it had been receiving the touches the whole time. However, in order to prevent the outer one from moving while the inner one is, we have to lock it without setting its isScrollEnabled to false, otherwise it'll stop receiving the touches and won't be able to pick up where the inner one left off when we want to scroll past the inner one's top or bottom.
That's done by assigning a UIScrollViewDelegate to the scrollviews, and implementing scrollViewDidScroll(_:) as shown:
class YourViewController: UIViewController, UIScrollViewDelegate {
private var mLockOuterScrollView = false
#IBOutlet var mOuterScrollView: CollaborativeScrollView!
#IBOutlet var mInnerScrollView: CollaborativeScrollView!
enum Direction {
case none, left, right, up, down
}
func viewDidLoad() {
mOuterScrollView.delegate = self
mInnerScrollView.delegate = self
mInnerScrollView.bounces = false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView is CollaborativeScrollView else {return}
let csv = scrollView as! CollaborativeScrollView
//determine direction of scrolling
var directionTemp: Direction?
if csv.lastContentOffset.y > csv.contentOffset.y {
directionTemp = .up
} else if csv.lastContentOffset.y < csv.contentOffset.y {
directionTemp = .down
}
guard let direction = directionTemp else {return}
//lock outer scrollview if necessary
if csv === mInnerScrollView {
let isAlreadyAllTheWayDown = (mInnerScrollView.contentOffset.y + mInnerScrollView.frame.size.height) == mInnerScrollView.contentSize.height
let isAlreadyAllTheWayUp = mInnerScrollView.contentOffset.y == 0
if (direction == .down && isAlreadyAllTheWayDown) || (direction == .up && isAlreadyAllTheWayUp) {
mLockOuterScrollView = false
} else {
mLockOuterScrollView = true
}
} else if mLockOuterScrollView {
mOuterScrollView.contentOffset = mOuterScrollView.lastContentOffset
}
csv.lastContentOffset = csv.contentOffset
}
}
And that's it. This will stop your outer scrollview from scrolling when you begin scrolling the inner one, and get it to pick up again when the inner one is scrolled all the way to one of its ends.

Related

How to enable/disable scrolling in UITableView?

I know about this command:
self.tableView.scrollEnabled = true
The question is: I want to lock scrolling according to the scrollView position. For this I do:
let topEdge = scrollView.contentOffset.y
let followersViewEdge = CGRectGetHeight(self.profileView.frame) - 50
if topEdge >= followersViewEdge {
self.tableView.scrollEnabled = true
}
it works, but the problem is that it does not lock or unlock the scrolling immediately. For locking or unlocking the UITableView scrolling I need to release my finger from the screen and scroll again. In this case it works.
I want to make it immediately, so it locks and unlocks the scrolling while I'm swiping on the screen. How can I do it?
UPDATE
My code works. It's not a problem. Problem is that I need to make these changes immediately, without releasing my finger from the screen.
So, do not answer me how to lock scrolling! I know how to do this.
Select tableview
choose Attribute insepector
In scroll view section, uncheck Scrolling Enabled property
Force the contentOffset to your max value while scrolling :
let topEdge = scrollView.contentOffset.y
let followersViewEdge = CGRectGetHeight(self.profileView.frame) - 50
if topEdge >= followersViewEdge {
self.tableView.contentOffset.y = followersViewEdge
}
You need to setting up the pan gesture recognizer for your table view and keep listening listener.
By you approach can lock it.
if topEdge >= followersViewEdge {
self.tableView.scrollEnabled = true
}
By the pan gesture recognizer, you can lock/unlock it.
if(self.tableView.scrollEnabled) {
self.tableView.scrollEnabled = false
} else {
self.tableView.scrollEnabled = true
}
Example
// Added the Pan Recognizer for capture the touches
UIPanGestureRecognizer *panReconizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panReconizer:)];
panReconizer.maximumNumberOfTouches = panReconizer.minimumNumberOfTouches = 1;
[self.tableView addGestureRecognizer:panReconizer];
- (void)panReconizer:(UIPanGestureRecognizer *)pan {
NSLog(#" .............. pan detected!! ...................");
if(self.tableView.scrollEnabled) {
self.tableView.scrollEnabled = false
} else {
self.tableView.scrollEnabled = true
}
}

How do I make a UIImage fade in and fade out as I scroll?

I have a stretchy header that I made following this tutorial http://blog.matthewcheok.com/design-teardown-stretchy-headers/. Anyway it's working perfectly but I'm having trouble making a UIView on top of it fade out as the view stretched and returning to original alpha as the view is returned to normal. Best I could come up with is this:
override func
scrollViewDidScroll(scrollView: UIScrollView) {
updateHeaderView()
var offset = scrollView.contentOffset.y
if offset < -170 {
headerBlurImageView?.alpha = max(0.0, offset - 95/35)
} else {
self.headerBlurImageView?.alpha = 1
}
}
But it barely works. There is no smooth transition between the alphas and when the view is returned to normal the alpha doesn't return. Any advice or hints?
Update: I managed to do the exact opposite of what I wanted :p Here's the code:
override func scrollViewDidScroll(scrollView: UIScrollView) {
updateHeaderView()
var height: CGFloat
var position: CGFloat
var percent: CGFloat
height = scrollView.bounds.size.height/2
position = max(-scrollView.contentOffset.y, 0.0)
percent = min(position / height, 1.0)
self.headerBlurImageView.alpha = percent
}
Take a look at the UIScrollViewDelegate protocol. there are a number of messages that relate to starting and ending dragging, and decelerating. You should be able to set your view controller up as the scroll view's delegate and implement some of those methods. I'd create a UIView animation that animates the header's alpha down when scrolling begins, and another UIView animation that animates it back to opaque once scrolling ends (or possibly once deceleration ends.)
Layout two UIImageViews on top of one another (in this example, bg is under bgB), then layout the UIScrollview at the very top. In the scrollviewDidScroll method, place the following code:
float off = cv.contentOffset.x;
float floatedIndex = off/cv.frame.size.width;
int left = floatedIndex;
int right = ceil(floatedIndex);
if (right>events.count-1){ right=events.count-1; }
float alpha = floatedIndex-left;
UIImage * leftImage = nil;
UIImage * rightImage = nil;
NSDictionary * ld = events[left];
NSDictionary * rd = events[right];
NSObject * o = ld[#"artwork"];
if ([o isKindOfClass:[UIImage class]]){
leftImage = ld[#"artwork"];
}
o = rd[#"artwork"];
if ([o isKindOfClass:[UIImage class]]){
rightImage = rd[#"artwork"];
}
bg.image = leftImage;
bgB.image = rightImage;
bgB.alpha = alpha;
The 'left' and 'right' ints correspond to indices in an array. There's a line checking that the scrollview cannot accidently go 'too far right' ie trying to find an index outside the bounds of the array.
Then I'm pulling data from an array called events, and checking for the existence of an image in the resulting dictionary (but you could use it for a straight up array of images), updating both UIImageviews and then fading the one on top (bgB) in and out depending on the scroll's content offset.
The 'animation' correlates with user-interaction, which is great, but sometimes it's better to use animation blocks with another scrollview delegate method.

Disable UITableView scrolling temporarily during drag

I am trying to figure out a way to temporarily (i.e. during a single drag gesture) disable scrolling on a UITableView and then re-enable it to have it pick up where it left off.
My reason is I have a gesture recognizer that is monitoring the drag, and if the user drags their finger above the top of the table, I want to resize the table upwards with their finger, to a point, and then stop resizing and continue scrolling again.
Naturally, I don't want the table to scroll while it is resizing, because that's effectively achieving the scroll itself (by moving the entire table view instead of the inner scrollable content), however I can't figure out how to do this in such a way that it allows the gesture to take effect again after a certain point (or if the user drags back down over the table).
Is there a way to temporarily disable/block a gesture without causing it to fail or cancel outright?
Perhaps I could write a subclass of UITableView that can intercept the gestures and ignore them as needed. What method should I override to do this?
Update:
I ended up approaching this in a different way, which is to simply adjust the contentOffset of the table view at each change of the gesture. I was afraid this might look "jittery" but it actually works quite smoothly. However I'll leave the question open as I'm still curious if this can be done.
At the request of #BrunoGalinari, here is the main part of my implementation of handling pan gestures on a UITableView without breaking the table view's intrinsic scrolling.
tableViewExpanded is a local property that switches between two layout states (expanded or not) and adjusts the bottomViewHeightConstraint constant appropriately. Setting it to itself just readjusts the constraint to one of the two valid values since it is also affected during the pan.
- (void)handlePan:(UIPanGestureRecognizer*)sender {
static CGFloat initialBottomViewY;
static CGFloat initialTableViewContentOffsetY;
static CGFloat initialTouchPointY;
CGPoint touchPoint = [sender locationInView:self.view];
CGFloat splitOffset = touchPoint.y - initialBottomViewY;
BOOL inEffect = ( sender == self.tableViewPan && touchPoint.y < initialBottomViewY ) || ( sender == self.mapViewPan && touchPoint.y > initialBottomViewY );
switch ( sender.state ) {
case UIGestureRecognizerStateBegan: {
initialBottomViewY = self.bottomView.y;
initialTableViewContentOffsetY = self.tableView.contentOffset.y;
initialTouchPointY = touchPoint.y;
break;
}
case UIGestureRecognizerStateEnded: {
self.dragVelocity = [sender velocityInView:self.view].y;
if ( inEffect ) {
if ( ABS( splitOffset ) > 60.f ) { // adjust
if ( sender == self.mapViewPan && touchPoint.y > initialBottomViewY )
self.tableViewExpanded = NO;
else if ( sender == self.tableViewPan && touchPoint.y < initialBottomViewY )
self.tableViewExpanded = YES;
else
self.tableViewExpanded = self.tableViewExpanded;
} else
self.tableViewExpanded = self.tableViewExpanded; // spring back
}
break;
}
case UIGestureRecognizerStateChanged: {
if ( inEffect ) {
self.tableView.contentOffset = CGPointMake( self.tableView.contentOffset.x, initialTableViewContentOffsetY + initialTouchPointY - initialBottomViewY );
self.bottomViewHeightConstraint.constant = self.view.height - touchPoint.y;
self.annotationToSelect = nil;
[self adjustMapAnimated:NO];
}
break;
}
default: {
break;
}
}
}
Here's what the window looks like, to get an idea of placement:
You can disable scrolling of a UITableView by setting:
table.scrollEnabled = NO;
When you are don't with you custom gesture, enable it:
table.scrollEnabled = YES;
It is a property of the parent class UIscrollView:
If the value of this property is YES , scrolling is enabled, and if it is NO , scrolling is disabled. The default is YES.
When scrolling is disabled, the scroll view does not accept touch events; it forwards them up the responder chain.

Interactive transition like Google Maps (iOS)

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!

Proper way to check if a sub view is outside of the content size of its parent UIScrollView

I have an app that has a UIScrollView with an array of thumbnails for videos. When you touch the thumbnail, the thumbnail begins playing the video. However, when you scroll the view out of visible screen area of the scrollview, the video (obviously) does not stop playing.
I want to be able to detect when the cell goes out of visible screen of the scrollview but I'm not sure what the proper way to observe this change is. I can think of several ways off the top of my head, but I want to do this the "proper" way. How should I go about this?
I know a method that checks if the one frame is inside the other. I am sure that you can use it in your case, but you need to device your own logic & place it accordingly. A basic outline guide is as follows:
if (CGRectContainsRect(self.view.bounds, videoThumbNailFrame)) {
// videoThumbnail is Completely Inside
}else
{
if (videoThumbNailFrame.origin.y < self.view.bounds.origin.y) {
//vertically outside
}
if (videoThumbNailFrame.origin.x < self.view.bounds.origin.x) {
// horizontally outside
}
}
May be you can choose to perform this on this delegate method of scrollView. -(void)scrollViewDidEndDecelerating:(UIView *)scrollView
Hope that helps to some extent.
There are 2 steps:
Objc:-
Calculate the visible Rect of the scrollView
CGRect visibleRect;
visibleRect.origin = scrollView.contentOffset;
visibleRect.size = scrollView.bounds.size;
Check if the current playing video frame is intersect with the visibleRect. If YES, just leave it play, if NO, we stop it.
if( CGRectIntersectsRect( visibleRect, videoFrame ) ) {
//do nothing
} else {
//stop the video
}
Swift:-
var visibleRect: CGRect
visibleRect.origin = scrollView.contentOffset
visibleRect.size = scrollView.bounds.size
if visibleRect.intersects(videoFrame) {
//do nothing
} else {
//stop the video
}

Resources