How to disable horizontal scrolling in UIScrollView? - ios

I have a scrollView of 280(w) x 350(h) and a content view of 840(w) x 405(h).
I switch between the views with a segmented control like this:
- (IBAction)segmentedClicked:(UISegmentedControl *)sender
{
CGFloat x = sender.selectedSegmentIndex * self.personalDetailsScrollView.frame.size.width;
CGPoint point = CGPointMake(x, 0);
[self.personalDetailsScrollView setContentOffset:point animated:YES];
}
I want to disable the regular horizontal scrolling so only the segmented buttons will scroll the view horizontally. The vertical scroll should stay active.
Tried to use -(void)scrollViewDidScroll and the solution offered here: (How to lock the horizontal scrolling of a scrollView in iOS) but it didn't work for me for some reason.

I haven't tried it, but this is the approach I'd take:
In UIScrollView, there is a property:
#property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer
So I'd make my own new UIPanGestureRecognizer extension and override:
- (CGPoint)translationInView:(UIView *)view
{
CGPoint tmp = [super translationInView:view];
return CGPointMake(0, tmp.y);
}
Add that gesture recognizer to the scrollview, then call:
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:youCustomOne];
It should work.

try this
- (IBAction)segmentedClicked:(UISegmentedControl *)sender
{
if(sender.selectedSegmentIndex == 0)
{
CGPoint point = CGPointMake(0, 0);
}
else
{
CGPoint point = CGPointMake(self.personalDetailsScrollView.frame.size.width, 0);
}
[self.personalDetailsScrollView setContentOffset:point animated:YES];
}

I know this is a late answer, but I see there is still no accepted answer, so I'll give it a go.
It sounds like what you are trying to do is use 1 controller to effectively hold 3 separate screens. You're trying to switch between the screens depending on what the user selects in a UISegmentedControl, and you don't want the user to be able to pan between the views by just horizontally swiping. Why not just have 3 different view controllers? I think that is the more expected way to setup what you are trying to do, and therefore you will find it much easier to implement.

Swift 3, Set UIScrollViewDelegate to your class and then use below code.
scrollView.isDirectionalLockEnabled = true
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x>0 {
webView.scrollView.contentOffset.x = 0.0
}
}

Related

iOS UIScrollView scrolling detection

I'm developing kind of UITextView copy with customizations. For now I need to implement selection cursors (see screenshot of original UITextView below)
As I discovered from Debug View Hierarchy Apple developers draw these dots on separate Window to avoid clipping, and when UIScrollView starts dragging they move these dots inside UITextView, when it stops dragging they move it back to separate window. The only problem with this approach is how can I detect when some of my TextView superview's are UIScrollView and they start/end scrolling? Setting delegate for each of UIScrollView-type superviews looks bad and will bring a lot of headache, cause I will need to manage several delegates if needed (and even detect there change). Any ideas?
/*
In your viewDidLoad or where ever you create the UITextView call this :[self checkParentViewOfTextView:textField];
*/
-(void)checkParentViewOfTextView:(UITextView*)txv {
if ([txv.superview isKindOfClass:[UIScrollView class]]) { // Check if the superview if UIScrollView
UIScrollView *superScroll =(UIScrollView*) txv.superview;
superScroll.delegate = self;// In order to call the delegate methods below
superScroll.tag = 5; // Set a tag to access the current scrollView at these delegate methods
}
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
//Any scrollView did begin scrolling
if (scrollView.tag == 5) {
//Actions for your scrollView
}
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
//Any scrollView did end scrolling
if (scrollView.tag == 5) {
//Actions for your scrollView
}
}
You can use the same scroll view delegate for all UIScrollViews.
scrollView1.delegate = self
scrollView2.delegate = self
etc...
Just implement the delegate methods and take different action if necessary for each scroll view. Either do this by referencing properties in the class or setting the tag.
func scrollViewDidScroll(scrollView: UIScrollView!) {
if scrollView.tag == 0 {
// Do stuff
} else {
// Do other stuff
}
}

Handling touches for nested UIScrollViews scrolling in the same direction

I have two nested UIScrollViews, both scrolling in the vertical direction. I need the outer scrollview to scroll to it's max range first before allowing the inner scrollview to scroll. The inner scrollview should not be scrollable until the outer scrollview has reached it's max range. Here's an illustration:
In the left diagram, a vertical drag inside of Scrollview B should move Scrollview A and Scrollview B should not be scrollable (but it still needs to be able to receive touches/taps). Once Scrollview A reaches it's max range (when Scrollview B gets to the top of the screen), then Scrollview B should scroll. This needs to work in one continuous motion.
I've attempted to toggle ScrollView B's scrollEnabled from ScrollView A's scrollViewDidScroll: delegate method, but this doesn't appear to be a viable solution because it doesn't work in one continuous motion (eg: The user needs to release and touch again after Scrollview B reaches the top of the screen).
What's the best way to implement this such that is works in one continuous motion?
I solved the problem in the following way. I am not really happy with it since it looks to me much too complicated, but it works (please note that the code below is a simplified, untested version of my code, which is due to a different UI more complicated):
I have 3 properties that control scrolling:
#property (nonatomic, assign) CGFloat currentPanY;
#property (nonatomic, assign) BOOL scrollA;
#property (nonatomic, assign) BOOL scrollB;
2-step scrolling:
Disable scrolling for B, and enable scrolling for A.
This allows to scroll A .
When A reaches its max position, disable scrolling for A, and enable scrolling for B:
-(void)scrollViewDidScroll: (UIScrollView *)scrollView {
if (scrollView.contentOffset.y >= self.maxScrollUpOffset) {
[scrollView setContentOffset:CGPointMake(0, self.maxScrollUpOffset) animated:NO];
self.scrollviewA.scrollEnabled = NO;
self.scrollviewB.scrollEnabled = YES;
self.scrollB = YES;
}
}
This gives the following effect:
When A is scrolled upwards it will stop scrolling when its max size is reached. However B will not start scrolling, since the pan gesture recognizer of A does not forward its actions to the pan gesture recognizer of B. Thus one has to lift the finger and to start a 2nd scrolling. Then, B will scroll. This gives the 2-step scrolling.
Continuous scrolling:
For continuous scrolling, B must scroll while the finger that started scrolling of A continues moving upwards. To detect this, I added a further pan gesture recognizer tho A, and enabled it to detect gestures simultaneously with the built-in gesture recognizers of A and B:
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer {
return YES;
}
In the action of this additional pan gesture recognizer, I compute the distance the finger has moved upwards after the scrolling limit of A has been reached. By this distance, B is then scrolled programmatically:
- (void)panGestureRecognizerAction:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state != UIGestureRecognizerStateChanged) {
self.currentPanY = 0;
self.scrollB = NO;
self.scrollA = NO;
} else {
CGPoint currentTranslation = [recognizer translationInView:self.scrollviewA];
CGFloat currentYup = currentTranslation.y;
if (self.scrollA || self.scrollB) {
if (self.currentPanY == 0) {
self.currentPanY = currentYup;
}
CGFloat additionalYup = self.currentPanY - currentYup;
if (self.scrollA) {
CGFloat offset = self.scrollviewA.scrollUpOffset + additionalYup;
if (offset >= 0) {
self.scrollviewA.contentOffset = CGPointMake(0, offset);
} else {
self.scrollviewA.contentOffset = CGPointZero;
}
} else if (self.scrollB){
self.scrollviewB.contentOffset = CGPointMake(0, additionalYup);
}
}
}
}
There is still a disadvantage:
If you start scrolling, lift the finger, and let the scrollView decelerate, it will behave like the 2-stage scrolling, since the additional pan gesture recognizer won’t recognize any pan gesture.
In my case I solved subclassing UIScrollView for the outer ScrollView.
class MYOuterScrollView: UIScrollView, UIGestureRecognizerDelegate
{
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
}
Your requirement that this should work in one continuous motion calls out the answer: you need to use just one UIScrollView, not two.
If you have just one scrollview you can then perform your magic by overriding the scrollview's layoutSubviews method and rejigging its content to perform your parallax effect based on the current value of contentOffset. Make sure that contentSize always reflect the full height (you can even update contentSize inside layoutSubviews if needs be).
For an architecture, take your existing diagram and just replace Scrollview B with View B.
The gesture recognizer for scroll view A would need to pass off to the gesture recognizer for scroll view B to have on continuous motion which I am pretty sure is not possible. Why not combine the content of the two scroll views instead and then you would have one continuous motion. This code will combine the content of scrollView A and B into just A.
UIScrollView* scrollViewA = ...;
UIScrollView* scrollViewB = ...;
NSArray* subviews = scrollViewB.subviews;
for (int i = 0; i < subviews.count; i++)
{
UIView* subview = [subviews objectAtIndex:i];
CGRect frame = subview.frame;
frame.origin.y += scrollViewA.contentSize.height;
subview.frame = frame;
[scrollViewA addSubview:subview];
}
CGSize size = scrollViewA.contentSize;
size.height += scrollViewB.contentSize.height;
scrollViewA.contentSize = size;
Same-Direction Scrolling
Same direction scrolling occurs when a UIScrollView that is a subview of a UIScrollView both scroll in the same direction. This is shown in the left image.
Follow this link if want to look in details https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html
Just set some attributes which are mentioned in above image. it works.

animate slide up and down UIView based on indexPath.row

right now I'm trying to move up and move down an UIView based on roll up or down in UITableView. So, if I rolled up the view, which means indexPath.row will keep incremented; the view will appear slide down until certain position and stopped. Then when I rolled down the table view, which means indexPath.row, decremented; the view will disappear by slide up outside the display view.
Do anyone have any idea what I need to code? Right now I'm just simply able to hide and unhide the view. I need the code for animate it slide up and down.
mark=indexPath.row;
flag=flag+1;
if (flag-mark>1) {
postBox.hidden=NO;
flag=indexPath.row;
}
else
postBox.hidden=YES;
Thanks in advance for your help ^^
If you want to move the view according to the movement of the UITableView, you can calculate the content offset of the UITableView and apply it to your moving view. Declare a ivar of type CGFloat named lastOffset.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat distance = scrollView.contentOffset.y - lastOffset;
lastOffset = scrollView.contentOffset.y;
[UIView animateWithDuration:0.2 animations:^{
self.movingView.frame = CGRectOffset(self.movingView.frame, 0.0f, distance);
}];
}

Swipe UITableView onto screen, keep swiping?

I want a tableview that starts offscreen and can scroll on-screen, reach the top, and keep scrolling. I've made a visual of the desired interaction below.
I've tried two things, and neither work exactly like I need.
The first thing I did was put the tableview in a scrollview, and move the scrollview when panning is detected on the tableview. This blocks touches in the tableview, and even if I could detect when the tableview hit the top of the screen, I'm not sure how I would continue scrolling.
The second thing I tried was to set the content size of the scrollview to the height of the tableview. This lets the tableview scroll, but I only seem to be able to receive touches in the initial small rectangle labeled "List Item 1". As the tableview scrolls, I can't grab the middle and scroll it anymore.
What's the best way to build this interaction? Edit: A map surrounds this bottom view to the left, right, and mostly top. When the bottom view is pulled up, the map is visible to the left and right.
1.)
2.)
3.) (and this keeps scrolling for as many items are as in the list.)
I guess you want something like this:
or this:
I laid out my table view over my map view. I set the table view's contentInset and contentOffset like this:
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = 44;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.tableView.contentInset = (UIEdgeInsets){ .top = self.view.bounds.size.height - self.tableView.rowHeight };
self.tableView.contentOffset = CGPointMake(0, -self.tableView.contentInset.top);
}
Note that, although the default row height is 44, tableView.rowHeight return -1 unless you explicitly set it. (Setting it to 44 in the storyboard doesn't change this.)
I used a subclass of UITableView in which I did two things:
I explicitly set self.backgroundColor = [UIColor clearColor]. I found that setting the background color to clear in the storyboard didn't work.
I overrode pointInside:withEvent::
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return point.y >= 0 && [super pointInside:point withEvent:event];
}
Note that you don't care about contentInset here. The table view's contentOffset.y (which is the same as its bounds.origin.y) is set to a negative number when its top content inset is exposed. It's set to 0 when the top of item 0 is at the top edge of the table view, which isn't the case when the item as at the bottom edge of the screen.
Another thing you might want is to prevent the table from stopping half-on the screen. If the user drags item 0 halfway up the screen, you want the table to scroll so item 0 is all the way at the top of the screen (if there are sufficient items), and if the user drags item 0 halfway down the screen, you want the table to scroll so just item 0 is showing.
I did that by making my view controller act as the table view's delegate and implementing this delegate method, inherited from UIScrollViewDelegate:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGFloat yMin = -self.tableView.contentInset.top;
CGFloat yMax = MIN(0, self.tableView.contentSize.height - self.tableView.bounds.size.height);
if (targetContentOffset->y < yMax) {
if (velocity.y < 0) {
targetContentOffset->y = yMin;
} else {
targetContentOffset->y = yMax;
}
}
}
That method is carefully written so that it works for tables too short to fill the screen vertically, and for tables that can fill the screen vertically.
I've uploaded my test project here: https://github.com/mayoff/tableView-over-mapview
Update for side-by-side tables
I don't think side-by-side tables is going to be a good user interface. I think it's going to be confusing. But here's how you do it.
The view hierarchy looks like this:
Root view
MKMapView
MyScrollView
ScrollContentView
MyTableView for first table
MyTableView for second table
MyTableView for third table
etc.
The map view and the scroll view have the same frames. The scroll view handles the sideways scrolling and each table view is independently scrollable vertically.
Since the scroll view should only capture touches that land in one of the table views, it needs a custom hitTest:withEvent: that returns nil for touches outside any of the table views:
#implementation MyScrollView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
return hitView == self ? nil : hitView;
}
#end
But this won't actually do the job, because (in my implementation) the scroll view has just one big subview, the ScrollContentView. So we need to do the same thing in ScrollContentView:
#implementation ScrollContentView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
return hitView == self ? nil : hitView;
}
That's sufficient to pass touches down to the map view if they land outside of the tables.
I also use ScrollContentView to lay out the tables and set the scroll view's content size:
- (void)layoutSubviews {
[super layoutSubviews];
// Layout of subviews horizontally:
// [gutter/2][gutter][subview][gutter][subview][gutter][subview][gutter][gutter/2]
// where 3 * gutter + subview = width of superview
CGSize superSize = self.superview.bounds.size;
CGFloat x = kGutterWidth * 3 / 2;
CGFloat subWidth = superSize.width - kGutterWidth * 3;
for (UITableView *subview in self.subviews) {
subview.frame = CGRectMake(x, 0, subWidth, superSize.height);
x += subWidth + kGutterWidth;
CGFloat topInset = superSize.height - subview.rowHeight;
subview.contentInset = (UIEdgeInsets){ .top = topInset };
subview.contentOffset = CGPointMake(0, -topInset);
}
x += kGutterWidth / 2;
self.frame = CGRectMake(0, 0, x, superSize.height);
((UIScrollView *)self.superview).contentSize = self.bounds.size;
_pageWidth = subWidth + kGutterWidth;
}
I also made my view controller be the scroll view's delegate, and implemented a delegate method to force the scroll view to stop on “page” (table) boundaries:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGFloat pageWidth = contentView.pageWidth;
// Force scroll view to stop on a page boundary.
CGFloat pageNumber = targetContentOffset->x / pageWidth;
if (velocity.x < 0) {
pageNumber = floor(pageNumber);
} else {
pageNumber = ceil(pageNumber);
}
pageNumber = MAX(0, MIN(pageNumber, contentView.subviews.count - 1));
targetContentOffset->x = pageNumber * pageWidth;
}
The result:
I've updated the git repository with this version.
You ought to be able to do this pretty easily by setting your table view’s top contentInset to something high (as sha suggested in the comments) and then making your UITableView a subclass so you can override -pointInside:withEvent:. Using that and the current contentOffset, you can determine whether the incoming event is inside the area you want to be scrollable, and return YES or NO accordingly; if you return NO, then the touch should fall through to the map view as intended.
Why not change this completely. You said you have a map "underneath" the tableview. So when scrolled up the map will be hidden over by the table view. I presume when you scroll down again the map will be revealed?
You should be able to do this by using the UITableView header. Either a section header or a table view header. They behave slightly differently upon scrolling.
I'd maybe do it this way...
Use a table view header on the table. In this header you place your map view.
By default this will be pinned to the top of the table so if you scroll the table up then the map will slide off the top of the screen with it.
However, if you then intercept the scroll view delegate method - (void)scrollViewDidScroll:(UIScrollView *)scrollView; then you can work out if the table is being scrolled upwards and offset the map view so it stays where it is.
i.e. if the table is scrolled to (0, 10) then offset the map to (0, -10) so it looks like it hasn't moved.
This will give you the scroll in and out feature of the tableview and keep the map in view and responding to touches.

Making two UIScrollView's scroll as one, or transferring inertia between scroll views

In our app you can build a question by searching for options from multiple third party sources. Most of these search results are displayed as full-width tableview cells, as their data suits that format (has a bunch of metadata text I can display next to the thumbnail).
In the case of images however, a collection view makes much more sense. But now I run into the problem of a vertical scrolling tableview containing a vertical scrolling collection view.
http://www.screencast.com/t/7Z48zkkW
I can make it work somewhat by capturing the viewDidScroll on the collection view and updating the parent scroll view instead at the appropriate offsets, but it only works well when the user is actively dragging the collection view.
self.collectionVC.scrollViewDidScroll = ^(UIScrollView *scrollView) {
#strongify(self);
if (self.tableView.contentOffset.y < self.scrollingHeightOffset && scrollView.contentOffset.y > 0) {
CGFloat maxheight = MIN(self.scrollingHeightOffset, self.tableView.contentOffset.y + scrollView.contentOffset.y);
self.tableView.contentOffset = CGPointMake(0, maxheight);
scrollView.contentOffset = CGPointMake(0, 0);
} else if (scrollView.contentOffset.y < 0 && self.tableView.contentOffset.y > -topGuide) {
CGFloat minheight = MAX(-topGuide, self.tableView.contentOffset.y + scrollView.contentOffset.y);
self.tableView.contentOffset = CGPointMake(0, minheight);
scrollView.contentOffset = CGPointMake(0, 0);
}
};
When 'flinging' the collection view, the scrolling stops abruptly, losing the inertia of the collection view. Touching the tableview to scroll has a different problem, as I'm not capturing that when it hits the end and scrolling the collection view instead.
Right now the collection view lives in a cell of the tableview, but it could also be a peer if necessary. I'm trying to determine the best way to make these two scrollviews appear as one.
As far as I know you did the right thing. viewDidScroll should be called several times even after user lift the finger and scroll view is returning to some position.
I use exact same approach to scroll couple scrollViews in sync when user is dragging one of them. Works perfectly.
You might want to check your calculation logic inside this method. See if it calls for all changes after user lift a finger, but you're positioning other scroll view in a wrong way.
I did this, let me know if it works for you
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView1.panGestureRecognizer addTarget:self action:#selector(didRecognizePanGesture:)];
[self.tableView2.panGestureRecognizer addTarget:self action:#selector(didRecognizePanGesture:)];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (self.selectedTableView == scrollView)
{
if (scrollView == self.tableView1)
{
self.tableView2.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.tableView2)
{
self.tableView1.contentOffset = scrollView.contentOffset;
}
}
}
- (void)didRecognizePanGesture:(UIPanGestureRecognizer*)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan)
{
if (self.selectedTableView != nil) //this is gonna make stop the previous scrollView
{
[self.selectedTableView setContentOffset:self.selectedTableView.contentOffset animated:NO];
}
self.selectedTableView = (UITableView*)gesture.view;
}
}

Resources