I have a UIScrollView that contains several dynamically resizing subviews. I can resize and layout the subviews just fine, but when I set the content size of the scroll view itself, the bottom subviews are clipped. Is there some reason why a scroll view's content size height should be larger than the sum of the heights of the views it contains?
Here's my situation in more detail:
I have a superview containing a UIScrollView containing several subviews. In the superview's layoutSubviews method, I calculated the needed size of each subview, then set the frames so the subviews are tiled vertically down the screen with a bit of space between them. When done, I set the height of the UIScrollView's content size to be the end of the last subview (origin.y + size.height). In theory, this means the bottom of the scroll view's content area should exactly line up with the bottom of the last subview.
But it doesn't. Instead, a nice chunk of the last subview is clipped. It's still there - if I scroll down I can see the remaining portion during the "bounce". The problem is even worse in landscape mode - a much larger portion of the bottom subview simply isn't visible.
The subviews are all being arranged and positioned properly. The problem is that the UIScrollView's contentSize seems to need to be significantly larger than the sum of the heights of the subviews (plus the space between them). This doesn't make any sense to me. Furthermore, the amount the size is "off" varies - I reuse this view several times with different subviews, and they're all off by a different amount. Therefore, simply adding a constant to the content view height won't help.
What is causing the content size (or my height calculations) to not function correctly?
Code:
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat width = self.bounds.size.width - [self subviewLeftMargin] - [self subviewRightMargin]; // All subviews have same width as parent view
CGFloat x = [self subviewLeftMargin]; // All subviews should start at the far left of the view
CGFloat y = [self spaceBetweenSubviews]; // Running tally of the y coordinate for the next view
/* Adjust the subviews */
for(UIView *view in self.theVariousSubviews) {
/* Resize the view with the desired width, then let it size its height as needed */
view.frame = CGRectMake(view.frame.origin.x, view.frame.origin.y, width, view.frame.size.height);
CGSize newSize = [view sizeThatFits:view.frame.size];
/* Set the origin */
//The subviews are positioned correctly, so this doesn't seem to be a problem
view.frame = CGRectMake(x, y, newSize.width, newSize.height);
/* Have the view refresh its own layout */
[view setNeedsLayout];
/* Update the y value for the next subview */
y += newSize.height + [self spaceBetweenSubviews];
}
/* Resize the scroll view to ensure it fits all of the content */
CGFloat scrollViewHeight = y;
self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, scrollViewHeight);
//Content size is set to the same total height of all subviews and spacing, yet it is too small. Why?
}
hi it seems to me that your calculation and resizing timing is wrong.
Without the missing code for the layout change I could not fully understand the problem.
What strikes me is that you are assigning view.frame twice and between the new calculation you intercept the process with sublayouting which might change some of the values your calculation is depending on.
I could only advice you to separate the calculation from layouting and not invoke methods while you are calculating. To bring light into it you should either drop a sample app with the missing calculation or for yourself add some NSLog statement showing you the frame origin size of any subview and the contentOffset for the scrollview.
On my experiences the scrollview is working properly in general so I would expect a bug within your code.
Related
How do you handle centering and scrolling content in a UIScrollView when the dimensions are both larger and smaller than the containing scroll view?
I have been trying to set values in layoutSubviews to handle the initial centering of the content, while still allowing for scrolling.
If the content is smaller in both dimensions, I can just set the frame and the image is properly centered for all rotations and orientations. Setting the contentInset will also work. contentOffset does not seem to work.
If the content is larger in both dimensions, I can set contentOffset for the initial display, and not modify it again to support scrolling.
What do I do if I have an image with one dimension larger, and the other smaller, than the scroll view?
contentOffset uses a CGPoint, and contentInset uses UIEdgeInsets (top, left, bottom, right). I have tried mixing positive and negative, since one dimension needs to be moved in and the other out, but haven't gotten anything to work.
My next thought is to resize the scroll view (and modify constraints I suppose) so that the content is never smaller than the container and use contentOffset.
I would really like to have a single approach that will work regardless of larger or smaller dimensions.
What is the best solution (a solution) to this problem?
After stepping back from this, getting a better understanding of UIScrollView, and rethinking, I have solved my problem.
For starters, layoutSubviews is the wrong way to go, at least for what I need.
Trying to resize the UIScrollView and update constraints seemed more trouble than it was worth.
For whatever reason, it didn't initially occur to me than I could use both contentOffset and contentInset at the same time (thought it was either/or for some reason), but that was my exact solution.
CGRect rectContent = CGRectMake(0, 0, image.size.width, image.size.height);
UIImageView *imageView = [[UIImageView alloc] initWithFrame:rectContent];
self.scrollView.contentSize = rectContent.size;
CGFloat fOffsetWidth = (rectContent.size.width < self.scrollView.bounds.size.width) ? (self.scrollView.bounds.size.width - rectContent.size.width)/2 : 0;
CGFloat fOffsetHeight = (rectContent.size.height < self.scrollView.bounds.size.height) ? (self.scrollView.bounds.size.height - rectContent.size.height)/2 : 0;
self.scrollView.contentInset = UIEdgeInsetsMake(fOffsetHeight, fOffsetWidth, fOffsetHeight, fOffsetWidth);
self.scrollView.contentOffset = CGPointMake((rectContent.size.width - self.scrollView.bounds.size.width)/2, (rectContent.size.height - self.scrollView.bounds.size.height)/2);
imageView.image = image;
[self.scrollView addSubview:imageView];
All image dimension possibilities (larger/smaller, one/both) are centered in the scroll view, and a larger image dimension is scrollable while a smaller dimension remains centered.
Perfect!
I'm writing an app in Objective-C using Xcode 6 and iOS 8. The app needs to be able to be deployed on an iPhone 5, 6, or 6+.
If you want to get straight to answering my question, jump down to the last sentence. If you want to understand why I have the question I do, or maybe how I can alter my UI layout in order to solve my problem another way, read on.
In one of my view controllers, I have a scroll view whose top is constrained to the bottom of the navigation bar, and whose bottom is constrained to the top of a table view. The table view's bottom is constrained to the bottom of the view controller's main view (i.e. to the bottom of the phone).
The scroll view contains subviews that expand/contract when the user taps on them. I want the scroll view to grow as its subviews grow, but obviously I don't want the scroll view to grow off screen because it looks bad and because it would cause unsatisfiable constraints (the table view's top--which is constrained to the bottom of the scroll view--would cross below its bottom--which is constrained to the bottom of the main view...this causes an error). So, I use the following code to make the scroll view resize itself according to its subviews sizes without growing right off the screen:
// The max height before the scroll view would go off screen, which would
// mess up the table view's constraints and cause all sorts of problems
CGFloat maxHeight = self.view.size.height
- self.navigationController.navigationBar.frame.size.height
- [UIApplication sharedApplication].statusBarFrame.size.height;
// The height of all the subviews in the scroll view.
CGFloat height = _scrollContentView.frame.size.height;
if (height > maxHeight) {
height = maxHeight;
}
self.scrollViewHeightConstraint.constant = height;
Now for the fun part. Originally, I called this code to re-evaluate and reset the size of the scroll view whenever I rotated the device from portrait to landscape, or vice versa. However, when I would rotate the phone from portrait to landscape, I was getting constraints errors. I determined that it was because I was calling this code after the rotation, when the main view's height was smaller, but the scroll view's height was still large (causing the table view's top to go below the bottom, etc. as I explained before). So, I just moved the code to be called before the rotation (I called the code in the viewWillTransitionWithSize:withTransitionCoordinator: method). This all makes sense so far.
However, now, the problem is that the navigation bar's height changes when the rotation occurs, but the viewWillTransitionWithSize:... method does not include any details on this change (it only gives the new size that the main view will be when rotation is completed, not the new size the navigation bar will be as well).
So, I need someway to determine the new size of the navigation bar before the device's orientation actually changes (just like I can determine the main view's new size before the device's orientation actually changes using the viewWillTransitionWithSize:... method).
Any ideas? TIA!
So, here's my work around in its simplest form:
/*
* This method gets called when the device is about to rotate.
*/
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
// Set the scroll view's height to 0 to avoid constraints errors as described
// in the question.
self.scrollViewHeightConstraint.constant = 0;
}
/*
* At the point when this method gets called, the device rotation has finished altering
* the frames of the views in this view controller, but the layout has not finished
* so nothing has changed on screen.
*/
- (void)viewWillLayoutSubviews
{
// The max height before the scroll view would go off screen, which would mess up
// the table view's constraints and cause all sorts of problems
CGFloat maxHeight = self.view.size.height
- self.navigationController.navigationBar.frame.size.height
- [UIApplication sharedApplication].statusBarFrame.size.height;
// The height of all the subviews in the scroll view.
CGFloat height = _scrollContentView.frame.size.height;
if (height > maxHeight) {
height = maxHeight;
}
// Reset the scroll view's height to the appropriate height.
self.scrollViewHeightConstraint.constant = height;
[super viewWillLayoutSubviews];
}
Inside a UIViewController, I need to have the bottom half scrollable. So I added a UIScrollView and positioned it halfway down the view's height. And in the viewDidAppear method, I have put the below two code lines to make it scrollable.
self.scrollView.contentSize = CGSizeMake(self.view.frame.size.width, self.scrollView.frame.size.height);
self.scrollView.frame = self.view.frame;
This way works if the scroll view fills the entire view, I've tested. But this method didn't work for my need. The scroll view would automatically move up and take up the entire screen. I assumed it was the second line of code which causes this.
So I removed the scroll view, added two UIViews to the view controller. To the bottom view, I added the UIScrollView. And in the viewDidAppear method, I have put the same two code lines changing the second line to refer the frame of the UIView that contains the scroll view..
self.scrollView.contentSize = CGSizeMake(self.view.frame.size.width, self.scrollView.frame.size.height);
self.scrollView.frame = self.containerView.frame;
But it wouldn't scroll either.
Can anyone please tell me how to do this correctly?
Thank you.
Dude, you keep setting the frame of the scrollView to something completely different from what you're actually trying to achieve.
If all you want to do is setup your scroll view so that it only occupies half the space then why dont you just set the frame so that the height only covers the portion of the screen that you want it to cover; and then set the x & y coordinates so that you draw the scroll view from the right position.
Do something like this:
//Shortcut to view's frame.
CGRect viewsFrame = self.view.frame;
/**
CGRectMake takes 4 parameters: x, y, width, height
x: is set to 0 since you want the scrollview to start from the left with no margin
y: you want the y position to start half way, so we grab the view's height and divide by 2
width: you want your scrollview to span from left to right, so simply grab the view's width
height: you want your scrollview's height to be half of your screen height, so get view's height and divide by 2.
*/
CGRect frameForSV = CGRectMake(0, viewsFrame.size.height/2, viewsFrame.size.width, viewsFrame.size.height/2);
UIScrollView *myScrollView = [[UIScrollView alloc] initWithFrame:frameForSV];
[self.view addSubview:myScrollView];
Then set your content size not based on an ansolute value, its best to have it based on the size of the content that's actually inside your scrollview so that your scrollview always scrolls to cover all your content inside it.
Also, remember that your scrollview will only scroll if the contentsize is greater than the scrollview's frame
UPDATE 1 after reading your comment in this post simply comment out any code in your viewController.m file related to your scrollview since youre setting up everything in interface builder.
This is the result:
I have an UIScrollview that is zoomable, the subview is one UIView (viewTexto) that contains an UILabel inside (messageLabel).
This is the code
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollViewtmp{
return viewTexto;
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollViewtmp withView:(UIView *)view atScale:(float)scale{
messageLabel.contentScaleFactor=scale;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, messageLabel.frame.origin.y + messageLabel.frame.size.heightt)];
}
With this code I can zoom, and the text is not blurry, there is no horizontal scroll but the size of the UILabel continues to be too large, so it is cut. I need that the width of the UILabel adopts to the scrollView width again as at the beginning.
I have read any question about UIScrollViews in SO and having found exactly what I need.
OK, so finally I found the answer as Ismael suggested I had to adjust the width, the problem was to find the equation.
The way the scaling works and the width of the scrollview's subviews is not obvious at the beginning.
Once you scale a UIView in a scrollview, and you want to have the same width for it, you have to divide the width of the element by the scale.
That is to say that if your initial width was 600, once you scale you can think that the width has changed and you only have to resize it again to 600, but it is not true. 600 is going to be multiplied by the scale automatically. So the right answer would be to resize it to 600 / scale.
Here we divide.
Now the code:
Everything happens in the method:
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollViewtmp withView:(UIView *)view atScale:(float)scale{}
The first thing was to get rid of the blurry fonts:
messageLabel.contentScaleFactor=scale;
In another method I saved the initial width of the UILabel messageLabel (inside scrollview), I called the variable "initialWidth", that was 600. This is important, because I started using the current messageLabel width once in the scrollViewDidEndZooming method.
To readjust the width of the subviews of the scrollview to 600, we only have to divide the initial width by the zoomscale, and readjust the label:
[messageLabel setFrame:CGRectMake(0,0,(initialWidth/scale), messageLabel.frame.size.height)];
[messageLabel sizeToFit];
At this point, we have a scrollview that can be zoomed, with a Label that readjust the text to the initial width of the scrollview, we don't have horizontal scrollbars, but we have a problem, the vertical scrollbars have a wrong height: we can only scroll a part of the text.
This was a difficult second problem to solve.
If you pass the messageLabel height to the contentsize, curiously it seems that it doesn'work, and even if I get a height multiplied by the scale in my NSLogs, it does not change the height of the scrolling, as if internally it was divided again. For example, if the initial height was 500, after scaling by 2, I get 1000 height, if I pass this value to the ContentSize it remains the same, as if it was divided again by 2.
So the solution was this time to multiply by the scale.
We only have to add this line:
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, (messageLabel.frame.origin.y+messageLabel.frame.size.height)*scale)];
As it is seen, the difficult part was to understand this mess with dividing or multiplying by the scale.
After you zoom, the contentSize of a UIScrollView increases proportionally (originalContentSize * zoomScale), so when you adjust it again, it will become smaller
Example:
Your scrollView.frame is (0, 0, 100, 100), and your contentSize is (100, 100).
After you zoom to 2.0, your contentSize will be (200, 200).
You are then putting it at (100, 100) manually, which equals to a contentSize of (50, 50) without zoom.
Since (contentSize.width <= scrollView.frame.size.width), there will be no horizontal scrolling, and the right-most part of your label is not visible.
Try not adjusting the contentSize. The scrolling will be possible then, and the user can reach the whole of the label
Edit: Re-reading your question, if you want the label to adjust and become visible entirely after the zoom, what you have to do in addition to adjusting the contentSize, is adjusting the messageLabel's frame. The label should wrap itself properly
Am fooling around with this question a couple of days now but no progress. What i want to do is quite simple i think:
I have an image of 320x60 which i use in the plain TableView which works oke as those cells take up the entire width (320) of the screen. The grouped cells in a TableView are 300 wide and have insets/margins left of 10 on the left and the right.
Can i somehow remove those insets/margins and let the grouped cell be 320 wide? I tried setting the content inset left to -10. That does "remove" the left margin but then it's still only 300 wide. Also tried editing the XML of the storyboard (I'm working with iOS 5 - Storyboards) but no joy.
This similar question here got answered as no it's not possible, hopfully something changed in 2+ years!:
Adjust cell width in grouped UITableView
PS i want to alter the width as the background images contain nice shadows, I've read that exesive use of shadows could mean performance issues. Also the shadow's are 5px extra around the border so that would mean -10px wide if I use the standard width.
Help much appreciated!
An untidy solution is to make the table view 340 pixels wide, and 10 pixels off the left edge of the screen.
A solution that involves changing properties of private classes is to make a UITableViewCell subclass, and override its layoutSubviews method. When I log the subviews, I find these:
"<UIGroupTableViewCellBackground: 0x95246b0; frame = (9 0; 302 45); autoresize = W; layer = <CALayer: 0x95226b0>>",
"<UITableViewCellContentView: 0x92332d0; frame = (10 0; 300 43); layer = <CALayer: 0x9233310>>",
"<UIView: 0x95248c0; frame = (10 0; 300 1); layer = <CALayer: 0x951f140>>"
What happens if we take those subviews and fill the entire bounds available?
- (void)layoutSubviews;
{
// By default the cell bounds fill the table width, but its subviews (containing the opaque background and cell borders) are drawn with padding.
CGRect bounds = [self bounds];
// Make sure any standard layout happens.
[super layoutSubviews];
// Debugging output.
NSLog(#"Subviews = %#", [self subviews]);
for (UIView *subview in [self subviews])
{
// Override the subview to make it fill the available width.
CGRect frame = [subview frame];
frame.origin.x = bounds.origin.x;
frame.size.width = bounds.size.width;
[subview setFrame:frame];
}
}
At this particular moment, on the iOS 5.1 simulator, this works. Now, some future version of iOS may restructure these classes, causing this method to catastrophically mangle the table layout. Your app could be rejected for changing the properties of UITableViewCellContentView... even though you're only modifying its frame. So, how much do you need to have your cells fill the table width?
You can the UITableView's Leading and Trailing Space constraints in the Size Inspector which is accessible via the Storyboard. I'm not sure when this was added, but setting the Leading Space Constraint to -10 and the Trailing Space Constraint to 10 will make the cells full width.