How to detect when a UIScrollView becomes scrollable? - objective-c

I have a UIScrollView sitting inside a popover. It is pinned to the edges of the popover using constraints, and it's full content is able to be displayed in the popover without scrolling (so the scroll view is not scrollable). When I tap a UITextField (located inside this scroll view), the keyboard slides up and the popover resizes. When the popover resizes, the UIScrollView becomes smaller and the content is now scrollable.
So my question is, how can I detect when my scroll view becomes scrollable?
P.S. I want to detect this so that I can set the delaysContentTouches property to YES when it is scrollable and to NO when the content is not scrollable.

You can check to see if the contentSize is larger than the scrollView's bounds.
CGSize contentSize = scrollView.contentSize;
CGSize boundsSize = scrollView.bounds.size;
if (contentSize.width > boundsSize.width && contentSize.height > boundsSize.height) {
// scrollable both vertically and horizontally
}
else if (contentSize.width > boundsSize.width) {
// scrollable horizontally
}
else if (contentSize.height > boundsSize.height) {
// scrollable vertically
}
else {
// same size or smaller, not scrollable
}

Related

What is the best way to disable horizontal scroll of UIScrollView?

In UIScrollView, I have content height and width greater than scrollview's size. So basically it can scroll horizontally and vertically. What I want is to scroll vertically only and I manage horizontally scroll using an UIButton by using below code.
[scrlViewQuestion setContentOffset:CGPointMake(questionWidth, 0) animated:YES];
Preventing horizontal scrollview, one can just set scrollview content width lesser than scrollview size but in my case scrollview content width is greater than its size? So what is the best way to solve this?
Content UIView width should be equal to the width of UIScrollView's superview for instance, not UIScrollView itself.
SWIFT 5
first, you should create a delegation class or extend the view control with UIScrollViewDelegate, and after, you should check the content offset with scrollViewDidScroll(_:) func and disable it with that:
here is a sample code:
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
.
.
.
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
}
}
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x != 0 {
scrollView.contentOffset.x = 0
}
}
}
Using this view hierarchy and these constraints
Step by Step explanation of the image
Place your ScrollView inside of a view(OuterView).
Place the view with your content(InnerView) inside the ScrollView
Align the ScrollViews 4 sides to the OuterViews 4 sides
ScrollView.leading = OuterView.leading
ScrollView.trailing = OuterView.trailing
ScrollView.top = OuterView.top
ScrollView.bottom = OuterView.bottom
Align the InnerViews 4 sides to the ScrollViews 4 sides
InnerView.leading = ScrollView.leading
InnerView.trailing = ScrollView.trailing
InnerView.top = ScrollView.top
InnerView.bottom = ScrollView.bottom
Set the InnerViews width and height equal to the OuterViews width, NOT the ScrollViews width.
InnerView.width = OuterView.width
Source
Set the contentSize of your scrollview to the width of your screen and the actual height of your scrollable content. Set the contentInset to zero for the top, bottom, and right. For the content inset on the left always choose the negated x coordinate of the position you currently want to display.
E.g. if your screen is 320 points wide, and you want to display content with horizontal scrolling fixed at x, but vertical scrolling allowed:
CGFloat screenWidth = 320.0;
CGSize actualContentSize = CGSizeMake(3000, 3000);
[scrlViewQuestion setContentSize:CGSizeMake(screenWidth, actualContentSize.height)];
[scrlViewQuestion setContentInset:UIEdgeInsetsMake(0, -x, 0, 0)];
swift 4. set the delegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x != 0 {
scrollView.contentOffset.x = 0
}
}
You can do this in storyboard.
Disable the Show Horizontal Indicator tab from the Scroll View's Identity Inspector.It will disable your horizontal scrolling.

Scrollview vertical only with fixed width in Autolayout

In swift how can I set the width of the scroll view to be the width of the screen size?
I tried setting right leading and trailing bounds, but my aspectscalefill image keeps stretching the entire frame.
I am trying to force the view to be the width of the screen and allow vertical scrolling only.
If you want to make a scroll view with auto layout try this. No code at all so you will have to drag things out of the library to the right of Xcode.
In your view controller drag and place the scroll view size it to whatever you want but it looks like you want to make it the size of the screen. Pin all edges to the edges of the view controller. Pin trailing, leading, top, and bottom.
Now, instead of placing your items in the scroll view, place another view in the scroll view. With this new view you will place all of your items. You will most likely have to move the view up or down to place them all and resize the view. You can then place whatever constraints you want on your items. When all items are in their place, set the frame of the new view back to x = 0 and y = 0.
You will then place constraints as follows. Select the new view and pin to top, bottom, trailing, and leading and then center in container. This will make a constraint that is vertical with some negative number. In the storyboard outline select this constraint and set it to zero.
You will now be able to scroll vertically. Let me know if you have any questions.
I created a single view application, and added a UIScrollView in the storyboard with four constraints (top, left, right, bottom). And I add the following code in the viewDidLoad(). Everything works just fine. My image is only scrolling vertically, but not horizontally. No additional settings are needed.
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Just as an example, I make a size with same with of the view
// and twice the height of the view.
let size = CGSizeMake(
self.view.frame.size.width,
self.view.frame.size.height * 2);
// Load an image and make a image view
let image = UIImage(named: "im.jpg")!
let imageView = UIImageView(image: image)
imageView.frame = CGRect(
origin: CGPoint(x: 0, y: 0),
size: size);
imageView.contentMode = UIViewContentMode.ScaleToFill;
// Add the image view to scroll view
scrollView.addSubview(imageView)
scrollView.contentSize = size;
}
Your problem i caused by the fact that you add your elements to the scrollView from the storyBoard.
In the storyBoard you need:
1) Add the scrollView, if you need to be in the width of the screen then set it's trailing and leading spaces to superView to 0
2) uncheck the "allow horizontal scrolling" in the attribute inspector
In your code
1) set the scrollView's contentSize to what ever size you want it to be.
1.1) in your code you need to set the size of the contentView like so
scroller.contentSize = CGSizeMake(your width, your height);
This is a very important step, the scrollView will not be able to scroll if you don't set the size. Think about the scrollView as the window and the contentView as the view. If the contentView if the same size as the scrollView then the scrollView can't scroll anywhere (all of the contentView fits into the scrollView) in order to create the scrolling you need to make the contentView bigger then the scrollView itself
2) start adding your elements to the scrollView's contentView by calling
[scroller addSubView:<your view>];
this will add your views to the scrollView's contentView and will be

UIScrollView within a UIScrollView, how to keep a smooth transition when scrolling between the two

I have the following layout (see below), which for most circumstances works just fine. The user is able to scroll within the blue UIScrollView (which for all intents and purposes is a UITableView, but this question generalises this), and then when they've reached the end of this scroll view, they can start scrolling again (they have to take their finger off, and on again, because the inner scroll view rubberbands otherwise), and the 'super' scroll view starts scrolling, revealing the rest of the image.
It's the whole (they have to take their finger off, and on again, because the inner scroll view rubberbands otherwise) that I don't want. Ideally, once the contained UIScrollView reaches the end of its content, I want the superview to take over scrolling straight away, so that the inner scroll view doesn't rubberband.
The same goes when the user is scrolling back up; when the red scrollview reaches the top of it's content, I want the inner blue scroll view to start scrolling up straight away, instead of the red scroll view rubberbanding at the top.
Any idea how? I am able to determine when the scroll views have reached the ends of their content, but I'm not sure how to apply this knowledge to achieve the effect I'm after. Thanks.
// Inner (blue) scroll view bounds checking
if (scrollView.contentOffset.y + scrollView.frame.size.height > scrollView.contentSize.height) { ... }
// Outer (red) scroll view bounds checking
if (scrollView.contentOffset.y < 0) { ... }
Yeah. Ive got a nifty trick for you.
Instead of having your red outlined view a scrollview make it a normal UIView that fills the screen. In that view lay out your scroll view (table view) and image view as they are in your illustration.
Place a scrollview that fills the bounds of the root view (i.e. also fills the screen) above all the other scrollview and image views. Set the content size of this view to be the total content height of all the views you want to scroll through. In otherwords there is an invisible scrollview sitting on top of all your other views and its content size height is inner scrollview (tableview) content size height + image view size height.
The heierarchy should look like this:
Then this scrollview on top that you have made with the really tall content size make its delegate be your view controller. Implement scrollViewDidScroll and we'll work some magic.
Scrollviews basically scroll by adjusting the bounds origin with funky formulas for momentum and stuff. So in our scrollviewDidScroll method we will simply adjust the bounds of the underlying views:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
//the scroll view underneath (in the container view) will have a max content offset equal to the content height
//but minus the bounds height
CGFloat maxYOffsetForUnderScrollView = self.underScrollView.contentSize.height - self.underScrollView.bounds.size.height;
CGRect scrolledBoundsForContainerView = self.view.bounds;
if (scrollView.contentOffset.y <= maxYOffsetForUnderScrollView) {
//in this scenario we are still within the content for the underScrollView
//so we make sure the container view is scrolled to the top and set the offset for the contained scrollview
self.containerView.bounds = scrolledBoundsForContainerView;
self.underScrollview.contentOffset = scrollView.contentOffset;
return;
}
//in this scenario we have scrolled throug the entirety of the contained scrollview
//set its offset to the max and change the bounds of the container view to scroll everything else.
self.underScrollView.contentOffset = CGPointMake(0, maxYOffsetForUnderScrollView);
scrolledBoundsForContainerView.origin.y = scrollView.contentOffset.y - maxYOffsetForUnderScrollView;
self.containerView.bounds = scrolledBoundsForContainerView;
}
You will find that as scrollViewDidScroll is called every frame of animation that this faux scrolling of the contained views looks really natural.
But wait! I hear you say. That scroll view on top now intercepts ALL touches, and the views underneath it need to be touched as well. I have an interesting solution for that as well.
Set the scrollview on top to be off screen somewhere (i.e. set its frame off screen, but still the same size.) and then in your viewDidLoad method you add the scrollview's panGestureRecogniser to the main view. This will mean that you get all the iOS natural scrolling momentum and stuff without actually having the view on the screen. The contained scroll view will now probably go juddery as its pan gesture recognizer will get called as well (they work differently to UIEvent handling) so you will need to remove it.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.view addGestureRecognizer:self.scrollview.panGestureRecognizer];
[self.underScrollview removeGestureRecognizer:self.underScrollView.panGestureRecognizer];
//further code to set up content sizes and stuff
}
I had fun making this so heres a link to the sample project on github:
https://github.com/joelparsons/multipleScrollers
EDIT:
To show the scrollbar for the top scrollview when its off the screen no matter where you put it you can set the scrollIndicatorInsets to an inset created like this:
CGPoint scrollviewOrigin = self.scrollview.frame.origin;
self.scrollview.scrollIndicatorInsets = UIEdgeInsetsMake(-scrollviewOrigin.y,0,scrollviewOrigin.y,scrollviewOrigin.x);
*caveat that the scrollview still has to be the right height but I'm sure you get the idea.
And then to make the bar draw outside the scrollview's visible bounds you have to turn off clips to bounds
self.scrollview.clipsToBounds = NO;
OMG. jackslash, you saved my life.
In my case, I need to use three depth of scroll views.
Parent Scroll View
that has a scroll view as one of the children
Child Scroll View: shown as 'Explanation Scroll View / Review Table View / Information Scroll View' in the below image)
Hidden Scroll View
which is distribute own content offset to 'Parent Scroll View' and 'Child Scroll View'
which has content size of whole flatten contents
Screenshot of view hierarchy
After setting view hierarchy, I just need to sync whole flatten content size and distribute content offset properly.
Observable.combineLatest(
overviewStackView.rx.observe(CGRect.self, #keyPath(UIStackView.frame)).unwrap(),
explanationScrollView.rx.observe(CGSize.self, #keyPath(UIScrollView.contentSize)).unwrap()
)
.subscribe(onNext: { [weak self] overviewStackViewFrame, explanationScrollViewContentSize in
guard let self = self else { return }
let totalContentHeight = overviewStackViewFrame.height + self.segmentedControl.frame.height + explanationScrollViewContentSize.height
self.hiddenScrollView.contentSize.height = totalContentHeight
})
.disposed(by: disposeBag)
hiddenScrollView.rx.didScroll
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
let currentHiddenScrollViewOffsetY = self.hiddenScrollView.contentOffset.y
let parentScrollViewMaxOffsetY = self.overviewStackView.frame.height
let expectedChildScrollViewOffsetY = max(currentHiddenScrollViewOffsetY - parentScrollViewMaxOffsetY, 0)
self.parentScrollView.contentOffset.y = min(parentScrollViewMaxOffsetY, currentHiddenScrollViewOffsetY)
self.explanationScrollView.contentOffset.y = expectedChildScrollViewOffsetY
})
.disposed(by: disposeBag)

How to use NSScrollview?

I can't figure out how to actually use NSScrollview. I dragged the scroll view object onto an NSWindow in the interface builder. I then dragged some NSButtons onto the scroll view. My question is:
How do I actually make it scroll down, for example, 2x the original height?
Of course the user can scroll automatically using their UI. I assume what you want to do is to scroll programmatically.
A bit of background: An NSScrollView has a documentView, which is the underlying view that the scroll view shows a part of, and a clipView, which is the view that is shown on the screen. So the clip view is like a window into the document view. To scroll programmatically you tell the document view to scroll itself in the clip view.
You have two options on how to scroll programmatically:
- (void)scrollPoint:(NSPoint)aPoint –– This scrolls the document so the given point is at the origin of the clip view that encloses it.
- (BOOL)scrollRectToVisible:(NSRect)aRect –– This scrolls the document the minimum distance so the entire rectangle is visible. Note: This may not need to scroll at all in which case it returns NO.
So, for example, here is an example from Apple's Scroll View Programming Guide on how to scroll to the bottom of the document view. Assuming you have an IBOutlet called scrollView connected up to the NSScrollView in your nib file you can do the following:
- (void)scrollToBottom
{
NSPoint newScrollOrigin;
if ([[scrollview documentView] isFlipped]) {
newScrollOrigin = NSMakePoint(0.0,NSMaxY([[scrollview documentView] frame])
-NSHeight([[scrollview contentView] bounds]));
} else {
newScrollOrigin = NSMakePoint(0.0,0.0);
}
[[scrollview documentView] scrollPoint:newScrollOrigin];
}

Scrollable UINavigationBar similar to Mobile Safari

My application uses a UINavigationController and the final view (detail view) lets you view an external website within the application using a UIWebView.
I'd like to free up some additional screen real estate when the user is viewing a webpage and wanted to emulate how Safari on iPhone works where their URL bar at the top scrolls up and off the screen when you're viewing content in the UIWebView that's below the fold.
Anyone have ideas on how to achieve this? If I set the navigationBarHidden property and roll my own custom bar at the top and set it and a UIWebView within a UIScrollView then there are scrolling issues in the UIWebView as it doesn't play nicely with other scrollable views.
Based on #Brian suggestion I made this code:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat height = navigationBar.frame.size.height;
CGFloat y = scrollView.bounds.origin.y;
if (y <= 0) {
CGRect frame = navigationBar.frame;
frame.origin.y = 0;
navigationBar.frame = frame;
} else if (tableView.contentSize.height > tableView.frame.size.height) {
CGFloat diff = height - y;
CGRect frame = navigationBar.frame;
frame.origin.y = -y;
navigationBar.frame = frame;
CGFloat origin = 0;
CGFloat h = height; // height of the tableHeaderView
if (diff > 0) {
origin = diff;
h = y;
}
frame = tableView.frame;
frame.origin.y = origin;
frame.size.height = tableView.superview.frame.size.height - origin;
tableView.frame = frame;
CGRect f = CGRectMake(0, 0, tableView.frame.size.width, h);
UILabel* label = [[UILabel alloc] initWithFrame:f];
tableView.tableHeaderView = label;
[label release];
}
}
My code has a UITableView but should work with any scrollable component. If you have other components than the navigationBar and the UIScrollView subclass, you should change the way the height of the scrollable component is calculated. Something like this:
frame.size.height = tableView.superview.frame.size.height - origin - otherComponentsHeight;
I needed to add a dumb tableHeaderView to have the desired behaviour. The problem was that when scrollViewDidScroll: is called the content has an offset, but the apparience in Mobile Safari is that the content is not scrolled until the navigationBar fully disappears. I tried first changing the contentOffset.y to 0, but obviously it didn't work since all the code relies on the scrolling mechanism. So I just added a tableHeaderView whose height is exactly the scrolled offset, so the header is never really seen, and the content appears to not scroll until the navigationBar fully disappears.
If you don't add the dumb tableHeaderView, then the scrollable component appears to scroll behind the navigationBar.
With the tableHeaderView, the scrollable component is actually scrolling (as seen in the scrollbar), but since there is a tableHeaderView whose height is exactly the same than the scrolled offset, the scrollable content appears to not be scrolling until the navigationBar fully disappears:
Have a delegate for the scrolling events in the UIWebView and when you initially start scrolling the UIWebView, have the UIWebView increase in height and have it's Y position decrease at the same time while simultaneously shifting the nav bar up in the Y direction. Once the nav bar has been completely shifted out of view, stop increasing the size of the UIWebView and just allow normal scrolling to occur.
This will give the illusion of the nav bar being part of the UIWebView as it scrolls off the screen.
Also, you'll need to do the reverse when you are scrolling in the opposite direction and are reaching the top of the content of the UIWebView.
Can't give you a straight answer, but have a look at iWebKit. Maybe that provides a solution. The demo, at least, contains a "full screen" item.