UITableView dragging distance with UIRefreshControl - ios

I'm having some trouble on implementing a UIRefreshControl on a UITableView.
Everything is working fine except the fact that I have to scroll something like 80% of the screen for the UIRefreshControl to get triggered. Sometimes I'm not even able to trigger it since there is a tab bar on the bottom of the screen, which cancels the scrolling movement when the finger reaches it.
I've looked at other apps, namely Apple's 'Mail', where the UIRefreshControl is triggered after scrolling only 30% of the screen.
What am I missing? Really need help on this one!
Thanks in advance

I had a similar problem and it's quite possible that's the same cause for you.
For me happens that I hided the scroll indicator making me unable to see the obvious cause of the problem: the UIScrollView's height is much greater than its superView...
Double check your UIScrollView's height because the "dragging distance" it's just a percentage of that height. Same goes for UITableView too, since it's a child class of UIScrollView.
EDIT:
Seems that this isn't the only way to reproduce this problem, since the required drag distance to trigger the refresher is calculated in a buggy way. Refer to this question for more info.
But in general it will happen if your UIScrollView's height is different than his parent container (e.g the screen itself).

You probably need to not use UIRefreshControl and just utilize scrollViewDidScroll (or tableViewDidScroll if a tableView) on handle your refresh accordingly since UIRefreshControl can't be modified.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if ((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height)
{
// Refresh from here
}
}

After some extensive testing and playing around with UIKit I have come to a conclusion.
TL;DR
UIRefreshControl isn't smart enough. To fix add this code in viewDidAppear or viewDidLayoutSubviews
let refreshControl = scrollView.refreshControl
scrollView.refreshControl = nil
scrollView.refreshControl = refreshControl
Test setup
Single UIViewController in a Storyboard with a UIView as its view which in turn has a UIScrollView as only subview which in turn has a single UIView as subview with top,right,bottom,left,width,height constraints equal to superview. The viewController has freeform size with 1200p height.
In the subclass of the UIViewController a UIRefreshControl is added by setting the UIScrollView#refreshControl to a new UIRefreshControl inside viewDidLoad.
When running the application in an iPhone X simulator and dragging the scrollView to perform a "Pull to refresh" one must drag considerably longer to make the refreshControl animating and send its notification it was pulled down.
The problem
One of my hypotheses was that the UIRefreshControl gets its dragging distance set once it is added to the scrollView and since AutoLayout hasen't updated the root view's subviews at viewDidLoad the scrollView has a height of 1180.0p instead of the correct 768.0p thus the refreshControl's dragging distance would be calculated for the height 1180.0p instead of 768.0p and thus become much longer than expected when running the app.
The solution
By updating the scrollView's refreshControl after the correct size for the scrollView has been set the correct dragging distance is calculated.
This update has to occur in a function where the correct size of the scrollView has been calculated, for example viewDidAppear and viewDidLayoutSubviews.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let refreshControl = scrollView.refreshControl
scrollView.refreshControl = nil
scrollView.refreshControl = refreshControl
}

Related

scrolling/resizing UITableView

I'll get right to the point.
I have a UIViewController that has two subviews in it. The top one (let's call it HeaderView from now one) is a custom UIView and the bottom one is a UITableView.
I have set them up in InterfaceBuilder so that the HeaderView has 0 margin from the left, top and right, plus it has a fixed height.
The UITableView is directly underneath with 0 margin from all sides.
My goal is to achieve a behaviour such that when I start scrolling the UITableView's content the HeaderView will start shrinking and the UITableView becomes higher without scrolling. This should go on until the HeaderView has reached a minimum height. After that the UITableView should start scrolling as normal. When scrolling down the effect should be reversed.
I have initially started this out using a UIScrollView instead of the UITableView and I have achieved the desired result. Here is how:
connect the UIScrollView to the outlet
#IBOutlet weak var scrollView: UIScrollView!
set the UIScrollViewDelegate in the controller's viewDidLoad() method
self.scrollView.delegate = self
and declared the UIViewController to conform to the protocol
intercept when the UIScrollView scrolls:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.adjustScrolling(offset: scrollView.contentOffset.y, scrollView: scrollView)
}
in my adjustScrolling(offset:scrollView:) method the "magic" happens
Now let's look at what happens in this method.
private func adjustScrolling(offset: CGFloat, scrollView: UIScrollView) {
// bind value between 0 and max header scroll
let actualOffset: CGFloat = offset < 0 ? 0 : (offset >= self.maxHeaderScroll ? self.maxHeaderScroll : offset)
// avoid useless calculations
if (actualOffset == self.currentOffset) {
return
}
/**
* Apply the vertical scrolling to the header
*/
// Translate the header up to give more space to the scrollView
let headerTransform = CATransform3DTranslate(CATransform3DIdentity, 0, -(actualOffset), 0)
self.header.layer.transform = headerTransform
// Adjust header's subviews to new size
self.header.didScrollBy(actualOffset)
/**
* Apply the corrected vertical scrolling to the scrollView
*/
// Resize the scrollView to fill all empty space
let newScrollViewY = self.header.frame.origin.y + self.header.frame.height
scrollView.frame = CGRect(
x: 0,
y: newScrollViewY,
width: scrollView.frame.width,
height: scrollView.frame.height + (scrollView.frame.origin.y - newScrollViewY)
)
// Translate the scrollView's content view down to contrast scrolling
let scrollTransform = CATransform3DTranslate(CATransform3DIdentity, 0, (actualOffset), 0)
scrollView.subviews[0].layer.transform = scrollTransform
// Set bottom inset to show content hidden by translation
scrollView.contentInset = UIEdgeInsets(
top: 0,
left: 0,
bottom: actualOffset,
right: 0
)
self.currentOffset = actualOffset
}
If I haven't forgotten anything this should be enough to achieve the desired effect. Let me break it down:
I calculate the actualOffset binding it between 0 and self.MaxHeaderScroll which is just 67 (I think, it's calculated dynamically but this doesn't really matter)
If I see that the actualOffset hasn't changed since the last time this function was called I don't bother to aplly any changes. This avoids some useless calculations.
I apply the scrolling to the header by translating it up with a CATransform3DTranslate on just the y axis by negative actualOffset.
I call self.header.didScrollBy(actualOffset) so that the HeaderView can apply some visual changes internally. This doesn't concearn the question though.
I resize the scrollView so that it keeps 0 margin from top and bottom now that the HeaderView is higher up.
I translate down the scrollView's content by the same actualOffset amount to contrast the scrolling. This piece is essential to the correct visual effect that I want to achieve. If I didn't do this, the scrollView would still resize correctly but the content would start scrolling right away, which I don't want. It should only start scrolling once the HeaderView reaches it's minimum height.
I now set a bottom inset in the scrollView so that I am able to scroll it all the way to the end. Without this, the last part of the scrollView would be cut off since the scrollView itself would think it reached the end of it's content.
Lastly I store the actualOffset for later comparison
As I said, this works fine. The problem arises when I switch from a UIScrollView to a UITableView. I assumed it would work since UITableView inherits from UIScrollView.
The only piece of code that doesn't work is the number 6. I don't really know what is going wrong so I will just list everything I have found out and/or noticed. Hopefully someone will be able to help me out.
in the case of the UIScrollView, in point 6, the scrollView.subviews[0] refers to a view that holds all the content inside it. When I change to UITableView this subview seems to be of the type UITableViewWrapperView which I could not find any documentation about, nor does XCode recognize it as a valid class. This is already frustrating.
if in point 6 I also give some translation on the x axis (let's say of 50) I can see an initial very quick translation that is immediately brought back to 0. This only happens when the UITableView starts scrolling, it doesn't go on while scrolling.
I have tried changing the frame of the subview in point 6 to achieve the desired result. Although the scrolling is correct, the top cells start disappearing as I scroll the UITableView. I thin this is because I am using dequeueReusableCell(withIdentifier:for:) to instatiate the cells and the UITableView thinks that the top cells aren't visible when they actually are. I wasn't able to work around this problem.
I have tried setting the self.tableView.tableHeaderView to a UIView of the actualOffset height to contrast scrolling but this gave a weird effect where the cells would not scroll correctly and when the UITableView was brought back to the initial position, there would be a gap on top. No clue about this either.
I know there's a lot here so please don't hesitate asking for more details. Thank you in advance.
I made something like this recently, so heres how I achieved it:
Make a UIView with a height constraint constant and link this to your view/VC, have you UITableview constrained to the VC's view full screen behind the UIView.
Now set your UITableViews contentInset top to the starting height of your 'headerView' now, in the scrollViewDidScroll you adjust the constant until the height of the header is at its minimum.
Here is a demo
If you just run it, the blue area is your 'header' and the colored rows are just any cell. You can autolayout whatever you want in the blue area and it should auto size and everything

Conserving Momentum from UIScrollView to inner UITableView

I'm looking to implement something very similar to the iOS Twitter Profile page, as seen here:
(source: twimg.com)
Based on what I can see, they have a UIView at the top, and a UIScrollView covering the entire view with a UITableView within the UIScrollView.
This is a tutorial on replicating it, and can be seen here: http://www.thinkandbuild.it/implementing-the-twitter-ios-app-ui/
The issue I've run into is how to maintain the momentum from scrolling on the UIScrollView vs. the UITableView. With the Twitter Profile page, you can scroll in one smooth swipe and it will move the UIScrollView up (showing the UITableView more) and any 'momentum' that is still there will start scrolling the UITableView.
I assume this must be done within the scrollViewDidScroll and check for any offset left over after reaching the bottom of the UIScrollView.
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView == self.myScrollView {
var maxOffset = 25.0
let offset = scrollView.contentOffset.y
self.myScrollView.frame = CGRectMake(0, -min(offset, maxOffset), view.frame.width, view.frame.height)
if offset - maxOffset > 0 {
self.myTableView.setContentOffset(CGPointMake(0,offset-maxOffset), animated:true)
}
}
This kind of works, although it certainly isn't smooth and doesn't appear as though it's maintaining momentum.
I don't think twitter's profile uses two scrollviews. It uses one tableview and adjusts the scroll indicator dynamically with the scrollIndicatorOffsets property. If you look closely at the scroll indicator you can see it shimmy a bit as you start to scroll up, which is consistent with this approach.

UITableViewController refreshControl is glitchy when UITableViewController frame.height is small

I have a problem that stems from the fact that UITableViewController refreshControl is glitchy when the frame of the UITableViewController is below a certain height.
As it stands, I have a UIViewController, and in it I have a ContainerView that embeds a UITableViewController. I want the height to be 50% of the screen.
When I use the refreshControl, I get this kind of behavior: The tableView jumps down at the very end when scrolling down. You'll notice it towards the end of this video when I decide to scroll down slowly.
This problem does not occur when the ContainerView frame is above a certain value. So, when the height is 75% of the screen, everything works perfectly and the refreshControl is smooth. When it is 50%, then that bug happens.
Two different things I have tried:
self.tableView.frame = CGRectMake(0, numOfPixelsToDropTableBy, self.tableView.frame.size.width, self.tableView.frame.size.height) is one thing that I tried. The problem with this is if you want to give the tableView rounded corners via the ContainerView and the fact that your ContainerView still takes up more space and this makes constraints for other elements awkward.
I went to the Storyboard and I basically had the top of the ContainerView where I wanted. Then, I had the bottom extend beyond the bottom of the screen to give the ContainerView a large enough height... but the user would never know. Except, they would know because now the tableView extends beyond the screen and I can't see the last few rows of my tableView.
Ultimately... I don't want to use a 3rd-party library, but I want a perfectly functioning refreshControl. How can I fix this?
1.I've created next architecture
2.Added some constraints
3.In TableViewController I've added next code
import UIKit
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl = UIRefreshControl(frame: CGRectZero)
self.refreshControl!.addTarget(self, action: "refresh:", forControlEvents: .ValueChanged)
}
func refresh(sender:UIRefreshControl)
{
self.refreshControl?.endRefreshing()
}
}
And uploaded example to github
IMPORTANT NOTE I've used Xcode 7 and Swift 2.
I managed to recreate your issue exactly by accident and managed to fix it, but at the cost of having no margins at all.
The jumping seems to happen if you use margin based constraints or any kind of margin for your container view. If you remove the margin relative part of the constraints, the jumping disappears.
Very strange, but seems to be the issue. As soon as I add any margin relative constraint for the container, the issue returns. Removing it and the display goes back to smooth scrolling.
This would seem to be a bug and I think you will need to raise a bug report with Apple.
Update:
Looking again, the issue seems to appear as soon as the container view is not the full width of the screen. Adding any sort of margin to the container view (via layout relative to margin or by setting a non zero offset on a constraint) results in the jumpy behavior.
Update:
Something would appear to be fundamentally broken with UITableView scrolling inside a container view which has any kind of margin. If you override the scrolling delegate, the content offset/bounds of the scroll view are being changed at the moment the refresh is about to trigger. Here is some debug showing the issue
Start pulling down:
Scroll bounds = {{0, -127.33333333333333}, {374, 423}}
Scroll pos = [0.000000,-127.333333]
Scroll bounds = {{0, -127.66666666666667}, {374, 423}}
Scroll pos = [0.000000,-127.666667]
Scroll bounds = {{0, -128.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-128.333333]
Ok before here ------->
Activity spinner becomes fully populated. Jump in scroll position upwards.
Scroll bounds = {{0, -104}, {374, 423}}
Scroll pos = [0.000000,-104.000000]
Scroll position corrects itself
Scroll bounds = {{0, -128.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-128.333333]
Scroll position jumps the other direction by the same amount
Scroll bounds = {{0, -151.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-151.333333]
Value changed target action fires. Bounds seem to reset (think 44 is height of refresh control
Scroll bounds = {{0, -44}, {374, 423}}
Scroll pos = [0.000000,-44.000000]
Corrects back
Scroll bounds = {{0, -151.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-151.333333]
Fully corrects to the right scroll position by jumping back.
Ok after here ------>
Scroll bounds = {{0, -128.66666666666666}, {374, 423}}
Scroll pos = [0.000000,-128.666667]
Scroll bounds = {{0, -129}, {374, 423}}
Scroll pos = [0.000000,-129.000000]
Scroll bounds = {{0, -129.33333333333334}, {374, 423}}
Scroll pos = [0.000000,-129.333333]
Scroll bounds = {{0, -129.66666666666666}, {374, 423}}
Scroll pos = [0.000000,-129.666667]
Scroll bounds = {{0, -130}, {374, 423}}
Conclusion
There seems to be no easy way I can find to work around this. I tried creating my own table view controller and the jumping goes away but is replaced by a different effect: that being that when you scroll down the top cell disappears, then reappears. I imagine it relates to the same internal issue, just being expressed differently.
Unfortunatley looks like you might have to put up with the effect or go for no margin. I would raise a bug report with Apple.
Only alternative option would be to create the margins in your UITableViewCells. You could make the cell content view have a clear background and introduce a left and right margin to your cells using an internal container view for your cell content. I think that may be you best chance.
And Finally...
Not to be defeated, you can apply a scaling transform to the navigation controller for the table view to create a margin doing the following in your table view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Add a scaling transform to the whole embedded controller view.
self.navigationController!.view.transform=CGAffineTransformMakeScale(0.9, 0.9);
}
This makes the view for the embedded controller appear 90% smaller so it has a margin around the border. Change the scale to to change the border size.
Not ideal, but works perfectly with no jump scrolling and has a border. It also leaves you totally free to use rounded corners etc as the whole content is scaled.
It seems that you've almost solved your problem (with a rough work around) using your off screen UIContainerView attempt. Give it another shot, but this time try:
Increasing the row count within the numberOfRowsInSection: by 1.
Inside your cellForRowAtIndexPath: method, set the last cell's rowHeight property to the distance your Container View is below the screen.
Step 2 won't work if you're using the method tableView:heightForRowAtIndexPath: - Instead, you'll need to set the height of the last cell using its index. Using this optional method can cause significant performance problems and can lead to lagged Refresh Controls too.
Following up on my comment:
To get UIRefreshControl it to play nicely with a UICollectionView or UITableView I've tried many things, but in the end the UIRefreshControl really only works well in a UITableViewController.
Then there is also an issue with adjusting the tintColor of the UIRefreshControl: sometimes it colors the spinner, sometimes it doesn't, sometimes the tintColor needs to be set inside an animation-block for some reason to take effect.
So I gave up on UIRefreshControl, and implemented my own solution. It is not as simple as setting a UIRefreshControl on a UITableViewController, but:
it works perfectly (or at least I have not been able to find uncovered edge-cases: if you find them, please file a pull-request)
you can implement any kind of loading view (something that rotates, something that bounces, maybe even a map view, or some UIKit Dynamics).
You can find it here:
JRTRefreshControl
I've found cases where simply setting an estimatedRowHeight on the tableView to match the rowHeight resolves the glitch. For reference, my setup was a UITableViewController contained inside a UIViewController with a fixed rowHeight of 140.

Can't scroll both UIScrollView in UITableViewCell and the UITableView itself

My pure AutoLayout UITableViewCell looks like this in Interface Builder:
UITableViewCell
|-> UITableViewCell.contentView
|-> UIView (ScrollViewContainerView)
|-> UIScrollView
|-> left (fixed)
|-> center (fill remaining)
|-> right (fixed)
The UIScrollView contains a left, center, and right UIView. left and right are both fixed width, while center expands to fill the remainder of the UIView. The UIScrollView constraints are to align all edges to ScrollViewContainerView. ScrollViewContainerView constraints are to align all edges to the UITableViewCell.contentView. I have a constraint on center's width to be a multiple of ScrollViewContainerView's width, so the UIScrollView scrolls left and right, but the height is fixed and does not scroll. Note that the UIScrollView has been subclassed to include this code so that the UITableView can detect a tap on the cell to toggle selection.
The issue is that I currently can either scroll the UITableView containing these UITableViewCells up and down or I can scroll the UIScrollViews in the UITableViewCells left and right, not both.
When ScrollViewContainerView.userInteractionEnabled == YES, I can't scroll the UITableView up and down, but I can scroll the UIScrollView left and right. When ScrollViewContainerView.userInteractionEnabled == NO, I can scroll the UITableView up and down, but I can't scroll the UIScrollView left and right. userInteractionEnabled == YES on everything else in the above hierarchy.
I can get away with having ScrollViewContainerView as a sibling view to the UIScrollView (making the UIScrollView the direct descent of contentView -- can't get rid of this view completely, because I require it to get the dimensions for the UIScrollView frame). In that case, the opposite handling with userInteractionEnabled holds.
I know I've done this before in other projects before, but starting fresh again, I can't seem to figure out what step I'm missing. Currently using Xcode 6 6A215l targeting iOS 8, though I have reproduced the issue under Xcode 5 targeting iOS 7.
It sounds like the scrollview is causing your tableview to not allow userInteraction when being scrolled. I'm sure that if you called - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView in the UIScrollView delegate (not sure for iOS 8), but you could just do
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
if(scrollView.dragging == YES) {
self.<scrollViewName>.userInteractionEnabled = NO;
}
}
This is untested code, but it's just a bit of help to get you where you need to go.
Hope it helps!
I met some similar problem.
I have a scrollView in tableViewCell. All works fine.
Until one day, someone told me that the tableView can't scroll up/down when finger is touched on the scrollView in 6p. Just in 6p, not in 5, 5s,or6.
This makes me almost crazy.
Finally, I set the scrollView's height smaller than the height in storyboard.
Biu ~ It works~~~
Still, I don't know why.
#user2277872's answer put me on the right track to look at the output of the UIScrollView delegate methods of the UIScrollView in my UITableViewCell subclass. Putting an NSLog() in scrollViewWillBeginDragging: made me notice that the UIScrollView was receiving scrolling events while I was trying to scroll the UITableView. My UIScrollView had a contentSize larger than its frame in both directions, but I've forced that view to only scroll horizontal, so ignored the height and reset it. That force was my undoing and I should have known it at the time -- the correct solution is to fix the frame height. If the UIScrollView doesn't think there is more vertical content, it will correctly forward the swipe up/down gesture to the UITableView.
While I attempt to figure out why my contentSize is too large when it wasn't before (thinking I'm missing a clipToBounds somewhere), what I'm doing to force horizontal scrolling temporarily is (in the UITableViewCell's subclass):
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGSize contentSize = self.scrollView.contentSize;
contentSize.height = self.frame.size.height;
self.scrollView.contentSize = contentSize;
}
EDIT: Actually, this is seemingly better than overriding drawRect. This would be in the UIScrollView subclass:
/*
* Lock to horizontal scrolling only.
*/
- (void)setContentSize:(CGSize)contentSize
{
[super setContentSize:CGSizeMake(contentSize.width, 1)];
}
The height struct member isn't too important, as long as it's guaranteed to be smaller than the frame.size.height of the UITableViewCell. Still hacky, still need to find why I could clip before and not now.

UIScrollView not scrolling

I have a UIScrollView which contains many UIImageViews, UILabels, etc... the labels are much longer that the UIScrollView, but when I run the app, I cannot click and scroll down...
Why might this be?
Thanks
It's always good to show a complete working code snippet:
// in viewDidLoad (if using Autolayout check note below):
UIScrollView *myScrollView;
UIView *contentView;
// scrollview won't scroll unless content size explicitly set
[myScrollView addSubview:contentView];//if the contentView is not already inside your scrollview in your xib/StoryBoard doc
myScrollView.contentSize = contentView.frame.size; //sets ScrollView content size
Swift 4.0
let myScrollView
let contentView
// scrollview won't scroll unless content size explicitly set
myScrollView.addSubview(contentView)//if the contentView is not already inside your scrollview in your xib/StoryBoard doc
myScrollView.contentSize = contentView.frame.size //sets ScrollView content size
I have not found a way to set contentSize in IB (as of Xcode 5.0).
Note:
If you are using Autolayout the best place to put this code is inside the -(void)viewDidLayoutSubviews method .
If you cannot scroll the view even after you set contentSize correctly,
make sure you uncheck "Use AutoLayout" in Interface Builder -> File Inspector.
You need to set the contentSize property of the scroll view in order for it to scroll properly.
If you're using autolayout, you need to set contentSize in viewDidLayoutSubviews in order for it to be applied after the autolayout completes.
The code could look like this:
-(void)viewDidLayoutSubviews
{
// The scrollview needs to know the content size for it to work correctly
self.scrollView.contentSize = CGSizeMake(
self.scrollContent.frame.size.width,
self.scrollContent.frame.size.height + 300
);
}
The answer above is correct - to make scrolling happen, it's necessary to set the content size.
If you're using interface builder a neat way to do this is with user defined runtime attributes. Eg:
Try to resize the content size to huge numbers. I couldn't understand why my scroll view doesn't scroll even when its content size seems to be bigger than control size. I discovered that if the content size is smaller than needed, it doesn't work also.
self.scrollView.contentSize = CGSizeMake(2000, 2000);
Instead of 2000 you can put your own big numbers. And if it works, it means that your content size is not big enough when you resize.
The delegate is not necessary for scroll view to work.
Make sure you have the contentSize property of the scroll view set to the correct size (ie, one large enough to encompass all your content.)
Uncheck 'Use Autolayout' did the trick for me.
Environment:
xCode 5.0.2
Storyboards
ios7
In my case I had to set delaysContentTouches to true because the objects inside the scrollView were all capturing the touch events and handling themselves rather than letting the scrollView itself handle it.
Set contentSize property of UIScrollview in ViewDidLayoutSubviews method. Something like this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = CGSizeMake(view.frame.size.width, view.frame.size.height)
}
if you are getting a message (IOS8 / swift) that viewDidLayoutSubviews does not exist, use the following instead
override func viewDidAppear(animated: Bool)
This fixed it for me
The idea of why scroll view is not scrolling because you set the content size for scrolling less than the size of the scroll view, which is wrong.
You should set the content size bigger than the size of your scroll view to navigate through it while scrolling.
The same idea with zooming, you set the min and max value for zooming which will applied through zooming action.
welcome :)
One small addition, all above are the actual reasons why your scroll view might not be scrolling but sometimes mindlessly this could be the reason specially when scrollview is added through code and not IB, you might have added your subviews to the parent view and not to the scrollview this causes the subview to not scroll
and do keep the content size set and bigger than parent view frame (duhh!!)
I made it working at my first try. With auto layout and everything, no additional code. Then a collection view went banana, crashing at run time, I couldn't find what was wrong, so I deleted and recreated it (I am using Xcode 10 Beta 4. It felt like a bug) and then the scrolling was gone. The Collection view worked again, though!
Many hours later.. this is what fixed it for me. I had the following layout:
UIView
Safe Area
Scroll view
Content view
It's all in the constraints. Safe Area is automatically defined by the system. In the worst case remove all constraints for scroll and content views and do not have IB resetting/creating them for you. Make them manually, it works.
For Scroll view I did: Align Trailing/Top to Safe Area. Equal Width/Height to Safe area.
For Content view I did: Align Trailing/Leading/Top/Bottom to Superview (the scroll view)
basically the concept is to have Content view fitting Scrollview, which is fitting Safe Area.
But as such it didn't work. Content view missed the height. I tried all I could and the only one doing the trick has been a Content view height created control-dragging Content view.. to itself. That defined a fixed height, which value has been computed from the Size of the the view controller (defined as freeform, longer than the real display, to containing all my subviews) and finally it worked again!
Add the UIScrollViewDelegate and adding the following code to the viewDidAppear method fixed it for me.
#interface testScrollViewController () <UIScrollViewDelegate>
-(void)viewDidAppear:(BOOL)animated {
self.scrollView.delegate = self;
self.scrollView.scrollEnabled = YES;
self.scrollView.contentSize = CGSizeMake(375, 800);
}
My issue was resolved by:
setting the contentSize on the scrollView to a large height
BUT also I had to fix top and/or bottom constraints on views within the scrollView, which meant the scroll indicators showed on screen but the content did not scroll
Once I removed top and/or bottom constraints bound to the safe area and/or superview, the views inside the scrollView could scroll again and didn't stay fixed to the top of bottom of the screen!
Hope this stops someone else from hours of pain with this particular issue.
yet another fun case:
scrollview.superview.userInteractionEnabled must be true
I wasted 2+hrs chasing this just to figure out the parent
is UIImageView which, naturally, has userInteractionEnabled == false
Something that wasn't mentioned before!
Make sure your outlet was correctly connected to the scrollView! It should have a filled circle, but even if you have filled circle, scrollView may not been connected - so double check! Hover over the circle and see if the actual scrollview gets highlighted! (This was a case for me)
//Connect below well to the scrollView in the storyBoard
#property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
Alot of the time the code is correct if you have followed a tutorial but what many beginners do not know is that the scrollView is NOT going to scroll normally through the simulator. It is suppose to scroll only when you press down on the mousepad and simultaneously scroll. Many Experienced XCode/Swift/Obj-C users are so use to doing this and so they do not know how it could possibly be overlooked by beginners. Ciao :-)
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
// Do any additional setup after the view
}
override func viewWillLayoutSubviews(){
super.viewWillLayoutSubviews()
scrollView.contentSize = CGSize(width: 375, height: 800)
}
This code will work perfectly fine as long as you do what I said up above
If none of the other solutions work for you, double check that your scroll view actually is a UIScrollView in Interface Builder.
At some point in the last few days, my UIScrollView spontaneously changed type to a UIView, even though its class said UIScrollView in the inspector. I'm using Xcode 5.1 (5B130a).
You can either create a new scroll view and copy the measurements, settings and constraints from the old view, or you can manually change your view to a UIScrollView in the xib file. I did a compare and found the following differences:
Original:
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" directionalLockEnabled="YES" bounces="NO" pagingEnabled="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Wsk-WB-LMH">
...
</scrollView>
After type spontaneously changed:
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" customClass="UIScrollView" id="qRn-TP-cXd">
...
</view>
So I replaced the <view> line with my original <scrollView> line.
I also replaced the view's close tag </view> with </scrollView>.
Be sure to keep the id the same as the current view, in this case: id="qRn-TP-cXd".
I also had to flush the xib from Xcode's cache by deleting the app's derived data:
Xcode->Window->Organizer->Projects, choose your project, on the Derived Data line, click Delete...
Or if using a device:
Xcode->Window->Organizer->Device, choose your device->Applications, choose your app, click (-)
Now clean the project, and remove the app from the simulator/device:
Xcode->Product->Clean
iOS Simulator/device->press and hold the app->click the (X) to remove it
You should then be able to build and run your app and have scrolling functionality again.
P.S. I didn't have to set the scroll view's content size in viewDidLayoutSubviews or turn off auto layout, but YMMV.
If your scrollView is a subview of a containerView of some type, then make sure that your scrollView is within the frame or bounds of the containerView. I had containerView.clipsToBounds = NO which still allowed me see the scrollView, but because scrollView wasn't within the bounds of containerView it wouldn't detect touch events.
For example:
containerView.frame = CGRectMake(0, 0, 200, 200);
scrollView.frame = CGRectMake(0, 200, 200, 200);
[containerView addSubview:scrollView];
scrollView.userInteractionEnabled = YES;
You will be able to see the scrollView but it won't receive user interactions.
adding the following code in viewDidLayoutSubviews worked for me with Autolayout. After trying all the answers:
- (void)viewDidLayoutSubviews
{
self.activationScrollView.contentSize = CGSizeMake(IPHONE_SCREEN_WIDTH, 620);
}
//set the height of content size as required
The straightforward programmatically way
To wrap it up
Create a UIScrollView
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
Use a Single Child View to Hold All of Your Content Subviews
private lazy var contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
Add your views
contentView.addSubview(firstSubView)
contentView.addSubview(lastSubView)
scrollView.addSubview(contentView)
view.addSubview(scrollView)
Usually, you only want your content to scroll in one direction. In most cases to scroll vertically. Therefore set the width of the content view to be the width of the scroll view.
NSLayoutConstraint.activate([
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
Attach four constraints (top, bottom, left, right) from our single content view to the scroll view.
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
Make sure you have constraints attached to all four sides of the content view so that it will expand to the size of your content.
// After Adding your subviews to the contentView make sure you've those two constraints set:
firstSubView.topAnchor.constraint(equalTo: contentView.topAnchor),
.
.
.
lastSubView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
Reference: Using UIScrollView with Auto Layout in iOS
After failing with the provided answers in this thread, I stumbled upon this article with the solution.
There are two things not intuitive about setting up the scrollview with autolayout:
The constraints you set up as margin between the contentview and scrollview do not influence the size of the contentview. They really are margins. And to make it work, the contentview should have a fixed size.
The trick to the fixed size is that you can set the width of the contentview equal to that of the scrollview's parent. Just select both views in the tree on the left and add the equal widths constraint.
This is the gist of that article. For a complete explanation, including illustrations, check it out.
I found that with this AutoLayout issue... if I just make the ViewController use UIView instead of UIScrollView for the class... then just add a UIScrollView myself... that it works.
I had the same issue in IB.
After setting the leading, trailing, top and bottom of the scrollView to its superView. I made the following changes to make the containerView scrollable, which worked.
To make the scrollView only scroll on horizontal direction make the constraint with scrollView's centerY = ContainerView's centerY
and to make it vertically scrollable make the scrollView's centerX = ContainerView's centerX
You don’t have to set the content size of the scroll view.
Technical Note TN2154
In case someone made the same mistake like me, I'd like to share my case.
In my case, I mistakenly add a constraint to one of the subviews of scrollview which makes the subview's space to the topLayoutGuide fixed, thus it's location can't be changed, so the scrollview can't be scrolled.

Resources