Zooming with UIScrollView using a strictly autolayout environment does not seem to work.
This is especially frustrating because the iOS 6 release notes certainly lead me to believe it should when the wrote about a "Pure Auto Layout approach" here http://developer.apple.com/library/ios/#releasenotes/General/RN-iOSSDK-6_0/_index.html
I looked the the WWDC 2012 slides for sessions 202, 228, and 232 and didn't see an answer for this.
The only question I've seen on the internet specifically for this issue is UIScrollView zooming with Auto Layout, but it doesn't provide code of the problem and there is no answer.
This user https://stackoverflow.com/users/341994/matt has given many great responses to UIScrollView autolayout questions and even linked to code on git hub, but I haven't been able to find anything that answers this issue there.
I have attempted to boil this issue down to the absolute minimum to make it clear.
I created a new single view application with a storyboard, and made no changes in the interface builder.
I added a large picture file to the project "pic.jpg".
SVFViewController.h
#import <UIKit/UIKit.h>
#interface SVFViewController : UIViewController <UIScrollViewDelegate>
#property (nonatomic) UIImageView *imageViewPointer;
#end
SVFViewController.m
#import "SVFViewController.h"
#interface SVFViewController ()
#end
#implementation SVFViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIScrollView *scrollView = [[UIScrollView alloc] init];
UIImageView *imageView = [[UIImageView alloc] init];
[imageView setImage:[UIImage imageNamed:#"pic.jpg"]];
[self.view addSubview:scrollView];
[scrollView addSubview:imageView];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
imageView.translatesAutoresizingMaskIntoConstraints = NO;
self.imageViewPointer = imageView;
scrollView.maximumZoomScale = 2;
scrollView.minimumZoomScale = .5;
scrollView.delegate = self;
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(scrollView,imageView);
NSLog(#"Current views dictionary: %#", viewsDictionary);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageView]|" options:0 metrics: 0 views:viewsDictionary]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[imageView]|" options:0 metrics: 0 views:viewsDictionary]];
}
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView{
return self.imageViewPointer;
}
#end
Notice I made a particular effort to make this as much like the sample code provided in the iOS 6 release notes, just doing the bare minimum to implement zooming.
So, the problem?
When you run this application and pan around in the scroll view, everything is good. But when you zoom the problem is obvious, the image flickers back and forth, and the placement of the image within the scroll view gets more wrong with every zoom.
It looks like there is battle going on for the content offset of the imageView, it seems it is being set to different values by two different things with every "zoom". (an NSLog of the content offset property of the imageView appears to confirm this).
What am I doing wrong here? Does anyone know how to property implement zooming within a UIScrollView in an purely autolayout environment. Is there an example of this anywhere out there?
Please help.
Once again, re-reading the iOS SDK 6.0 release notes I found that:
Note that you can make a subview of the scroll view appear to float (not scroll) over the other scrolling content by creating constraints between the view and a view outside the scroll view’s subtree, such as the scroll view’s superview.
Solution
Connect your subview to the outer view. In another words, to the view in which scrollview is embedded.
And applying constraints in following way I've got it work:
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageView(width)]" options:0 metrics:#{#"width":#(self.imageViewPointer.image.size.width)} views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[imageView(height)]" options:0 metrics:#{#"height":#(self.imageViewPointer.image.size.height)} views:viewsDictionary]];
The issues that occurs is the changing of location of the imageview during the zoom process. The origin location of the imageview will change to be a negative value during the zoom. I believe this is why the jerky movement occurs. As well, after the zoom is complete the imageView is still in the wrong location meaning that scrolls will appear to be offset.
If you implement -(void) scrollViewDidZoom:(UIScrollView *)scrollView and log the frame of the UIImageView during this time you can see its origin changing.
I ended up making things work out by implementing a strategy like this
And in addition changing the frame of the contentView while zooming
-(void) scrollViewDidZoom:(UIScrollView *)scrollView {
CGRect cFrame = self.contentView.frame;
cFrame.origin = CGPointZero;
self.contentView.frame = cFrame;
}
These solutions all kinda work. Here is what I did, no hacks or subclasses required, with this setup:
[view]
[scrollView]
[container]
[imageView]
[imageView2]
In IB, hook up top, leading, bottom and trailing of scrollView to view.
Hook up top, leading, bottom and trailing of container to scrollView.
Hook up center-x and center-y of container to center-x and center-y of scrollView and mark it as remove on build time. This is only needed to silence the warnings in IB.
Implement viewForZoomingInScrollView: in the view controller, which should be scrollView's delegate, and from that method return container.
When setting the imageView's image, determine minimum zoom scale (as right now it will be displayed at the native size) and set it:
CGSize mySize = self.view.bounds.size;
CGSize imgSize = _imageView.image.size;
CGFloat scale = fminf(mySize.width / imgSize.width,
mySize.height / imgSize.height);
_scrollView.minimumZoomScale = scale;
_scrollView.zoomScale = scale;
_scrollView.maximumZoomScale = 4 * scale;
This works for me, upon setting the image zooms the scroll view to show the entire image and allows to zoom in to 4x the initial size.
Let say you have in storyboard "UIImageView" inside "UIScrollView" inside "UIView".
Link all constraints in "UIScrollView" with the view controller + the two constraints in UIView (Horizontal Space - Scroll View - View & Horizontal Space - Scroll View - View).
set the view controller AS delegate for the "UIScrollView".
Then implement this code:
#interface VC () <UIScrollViewDelegate>
#property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
#property (weak, nonatomic) IBOutlet UIImageView *imageView;
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray* constraints;
#end
#implementation FamilyTreeImageVC
- (void)viewDidLoad {
[super viewDidLoad];
[self.scrollView removeConstraints:self.constraints];
}
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
-(void) scrollViewDidZoom:(UIScrollView *)scrollView {
CGRect cFrame = self.imageView.frame;
cFrame.origin = CGPointZero;
self.imageView.frame = cFrame;
}
I had the same problem when trying to implement zoom from a storyboarded project using only scrollView.
I fixed it by adding a separate pinch gesture recogniser. I just dragged it from the toolbox onto my scene. Then I connected it to an action I called "doPinch" that implements the zoom. I connected it to an outlet I called "pinchRecognizer" so that I could access its scale property. This seems to override the built in zoom of the scrollView and the jumpiness disappears. Maybe it does not make the same mistake with origins, or handles that more gracefully. It is very little work on top of the layout in IB.
As soon as you introduce the pinch gesture recogniser to the scene you do need both the action and viewForZoomingInScrollView methods. Miss out either and the zooming stops working.
The code in my view controller is this:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.zoomableImage;
}
- (IBAction)doPinch:(id)sender
{
NSLog(#"In the pinch action now with scale: %f", self.pinchRecognizer.scale);
[scrollView setZoomScale:self.pinchRecognizer.scale animated:NO];
}
This very basic implementation does have a side effect: when you come back to a zoomed image and zoom some more the value of scale is 1.0f so it jumps back to the original scale.
You can sort this out by introducing a property "currentScale" to track the scale and set the pinch gesture recogniser scale when you start zooming again. You need to use the state property of the gesture recogniser:
- (IBAction)doPinch:(id)sender
{
NSLog(#"In the pinch action now with scale: %f", self.pinchRecognizer.scale);
NSLog(#"Gesture recognizer state is: %d", self.pinchRecognizer.state);
switch (self.pinchRecognizer.state)
{
case 1:
NSLog(#"Zoom begins, with current scale set to: %f", self.currentScale);
[self.pinchRecognizer setScale:self.currentScale];
break;
case 3:
NSLog(#"Zoom ends, with pinch recognizer scale set to: %f", self.pinchRecognizer.scale);
self.currentScale = self.pinchRecognizer.scale;
default:
break;
}
[scrollView setZoomScale:self.pinchRecognizer.scale animated:NO];
}
So this is what I managed to work out.
Here's the original with my changes:
#interface ScrollViewZoomTestViewController () <UIScrollViewDelegate>
#property (nonatomic, strong) UIImageView* imageViewPointer;
// These properties are new
#property (nonatomic, strong) NSMutableArray* imageViewConstraints;
#property (nonatomic) BOOL imageViewConstraintsNeedUpdating;
#property (nonatomic, strong) UIScrollView* scrollViewPointer;
#end
#implementation ScrollViewZoomTestViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIScrollView *scrollView = [[UIScrollView alloc] init];
UIImageView *imageView = [[UIImageView alloc] init];
[imageView setImage:[UIImage imageNamed:#"pic.jpg"]];
[self.view addSubview:scrollView];
[scrollView addSubview:imageView];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
imageView.translatesAutoresizingMaskIntoConstraints = NO;
self.imageViewPointer = imageView;
// New
self.scrollViewPointer = scrollView;
scrollView.maximumZoomScale = 2;
scrollView.minimumZoomScale = .5;
scrollView.delegate = self;
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(scrollView, imageView);
NSLog(#"Current views dictionary: %#", viewsDictionary);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|" options:0 metrics: 0 views:viewsDictionary]];
// Saving the image view width & height constraints
self.imageViewConstraints = [[NSMutableArray alloc] init];
// Constrain the image view to be the same width & height of the scroll view
[_imageViewConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageView(scrollView)]|" options:0 metrics: 0 views:viewsDictionary]];
[_imageViewConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[imageView(scrollView)]|" options:0 metrics: 0 views:viewsDictionary]];
// Add the image view constraints to the VIEW, not the scroll view
[self.view addConstraints:_imageViewConstraints];
// Flag
self.imageViewConstraintsNeedUpdating = YES;
}
So to recap here, I'm adding all of the constraints to self.view, saving the constraints set on the UIImageView in a NSMutableArray property, and setting a flag that the UIImageView constraints need updating.
These initial constraints on UIImageView work to set it up to start with. It will be the same width & height as the UIScrollView. However, this WON'T allow us to zoom the image view. It will keep it the same width / height as the scroll view. Not what we want. That's why I'm saving the constraints and setting the flag. We'll take care of that in a minute.
Now, set the view for zooming:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageViewPointer;
}
Ok, so now we need to actually allow us to zoom. I'm removing the initial UIImageView constraints and adding some new ones, this time constraining to the UIScrollView's contentSize width & height:
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
if(_imageViewConstraintsNeedUpdating)
{
// Remove the previous image view constraints
[self.view removeConstraints:_imageViewConstraints];
// Replace them with new ones, this time constraining against the `width` & `height` of the scroll view's content, not the scroll view itself
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_scrollViewPointer, _imageViewPointer);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_imageViewPointer(width)]|" options:0 metrics:#{#"width" : #(_scrollViewPointer.contentSize.width)} views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_imageViewPointer(height)]|" options:0 metrics:#{#"height" : #(_scrollViewPointer.contentSize.height)} views:viewsDictionary]];
self.imageViewConstraintsNeedUpdating = NO;
}
}
#end
We can't set the constraints up like this in -viewDidLoad because the image hasn't been rendered into the UIImageView yet, so UIScrollView's contentSize will be {0,0}.
This seems pretty hacky to me, but it does work, it does use pure Auto Layout, and I can't find a better way to do it. Seems to me like Apple needs to provide a better way to zoom content in a UIScrollView AND use Auto Layout constraints.
Related
I have a very simple UIViewController that I am using to try to better understand constraints, auto layout, and frames. The view controller has two subviews: both are UIViews that are intended to either sit side-by-side or top/bottom depending on the device orientation. Within each UIView, there exists a single label that should be centered within its superview.
When the device is rotated, the UIViews update correctly. I am calculating their frame dimensions and origins. However, the labels do not stay centered and they do not respect the constraints defined in the storyboard.
Here are screenshots to show the issue. If I comment out the viewDidLayoutSubviews method, the labels are perfectly centered (but then the UIViews are not of the correct size). I realize that I could manually adjust the frame for each of the labels, but I am looking for a way to make them respect their constraints within the newly resized superviews.
Here is the code:
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic) CGFloat spacer;
#end
#implementation ViewController
#synthesize topLeftView, bottomRightView, topLeftLabel, bottomRightLabel;
- (void)viewDidLoad {
[super viewDidLoad];
topLeftLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
bottomRightLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.spacer = 8.0f;
}
- (void)viewDidLayoutSubviews
{
if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) {
[self setupTopLeftForLandscape];
[self setupBottomRightForLandscape];
} else {
[self setupTopLeftForPortrait];
[self setupBottomRightForPortrait];
}
}
- (void) setupTopLeftForPortrait {
CGRect frame = topLeftView.frame;
frame.origin.x = self.spacer;
frame.origin.y = self.spacer;
frame.size.width = self.view.frame.size.width - 2*self.spacer;
frame.size.height = (self.view.frame.size.height - 3*self.spacer) * 0.5;
[topLeftView setFrame:frame];
}
- (void) setupBottomRightForPortrait {
CGRect frame = bottomRightView.frame;
frame.origin.x = self.spacer;
frame.origin.y = topLeftView.frame.size.height + 2*self.spacer;
frame.size.width = topLeftView.frame.size.width;
frame.size.height = topLeftView.frame.size.height;
[bottomRightView setFrame:frame];
}
- (void) setupTopLeftForLandscape {
CGRect frame = topLeftView.frame;
frame.origin.x = self.spacer;
frame.origin.y = self.spacer;
frame.size.width = (self.view.frame.size.width - 3*self.spacer) * 0.5;
frame.size.height = self.view.frame.size.height - 2*self.spacer;
[topLeftView setFrame:frame];
}
- (void) setupBottomRightForLandscape {
CGRect frame = bottomRightView.frame;
frame.origin.x = self.topLeftView.frame.size.width + 2*self.spacer;
frame.origin.y = self.spacer;
frame.size.width = topLeftView.frame.size.width;
frame.size.height = topLeftView.frame.size.height;
[bottomRightView setFrame:frame];
}
#end
Generally it’s a bad idea to mix frames with Auto Layout. (The exception is a view hierarchy that uses constraints containing a view that doesn’t, which then doesn’t use any constraints from that point down [and additional caveats]). One big problem is the constraint system generally won’t get any information from setFrame.
Another rule of thumb is that setFrame and the traditional layout tree is calculated before the constraint system. This may seem counter intuitive with the first part, but remember that 1) in the traditional layout tree the views lay out their subviews and then call layoutSubviews on them, so each one’s superview frame is set before it lays itself out but 2) in the constraint system, it tries to calculate the superview frame from the subviews, bottom-up. But after getting the information bottom up, each subview reporting up info, the layout work is done top-down.
Fixing
Where does that leave you? You’re correct that you need to set this programmatically. There’s no way in IB to indicate you should switch from top-bottom to side-to-side. Here's how you can do that:
Pick one of the rotation and make sure all constraints are set up
the way you want it in Interface builder- for example, each colored
view puts 8 points (your spacer view) from superview. The “clear constraints” and
“update frames" buttons in the bottom will help you and you’ll want to click
it often to make sure it’s in sync.
Very important that the top-left view only be connected to the
superview by the left(leading) and top sides, and the bottom right
only connected by the right(trailing) and bottom sides. If you clear
the sizes setting the height and width fixed, this will produce a
warning. This is normal, and in this case can be solved by setting
“equal widths” and”equal heights” and part of step 3 if necessary.
(Note the constant must be zero for the values to be truly equal.)
In other cases we must put a constraint and mark it “placeholder” to
silence the compiler, if we’re sure we'll be filling information but the compiler doesn’t know that.
Identify (or create) the two constraints that links the right/bottom
view to something to the left and to the top. You might want to use the object browser to the left of IB. Create two outlets in
the viewController.h using assistant editor. Will look like:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomViewToTopConstraint;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *rightViewToLeftConstraint;
Implement updateConstraints in the viewController. Here’s where the
logic will go:
.
-(void)updateViewConstraints
{
//first remove the constraints
[self.view removeConstraints:#[self.rightViewToLeftConstraint, self.bottomViewToTopConstraint]];
if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) {
//align the tops equal
self.bottomViewToTopConstraint = [NSLayoutConstraint constraintWithItem:self.bottomRightView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.topLeftView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
//align to the trailing edge by spacer
self.rightViewToLeftConstraint = [NSLayoutConstraint constraintWithItem:self.bottomRightView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.topLeftView
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:self.spacer];
} else { //portrait
//right view atached vertically to the bottom of topLeftView by spacer
self.bottomViewToTopConstraint = [NSLayoutConstraint constraintWithItem:self.bottomRightView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.topLeftView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:self.spacer];
//bottom view left edge aligned to left edge of top view
self.rightViewToLeftConstraint = [NSLayoutConstraint constraintWithItem:self.bottomRightView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.topLeftView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0];
}
[self.view addConstraints:#[self.rightViewToLeftConstraint, self.bottomViewToTopConstraint]];
[super updateViewConstraints];
}
Since you can’t change constraints after they’re added (except the constant) we have to do this remove-add step. Notice the ones in IB might as well be placeholders, since we’re removing them every time (we could check first). We could modify the constant to some offset value, for example relating to the superview by spacer + topViewHight + spacer. But this mean that when auto layout goes to calculate this view, you’ve made assumptions based on some other information, which could have changed. Swapping out the views and changing what they relate the factors that are meant to change each other connected.
Note that because Auto Layout will use the constraints here when passing information up, first we modify them, then we call super. This is calling the super class private implementation to do the calculations for this view, not the superview of this view in the view hierarchy, although in fact the next step will be further up the tree.
I have an issue with adding a NSLayoutConstraint. I'd like to update the height of an UICollectionView so that all cells fit in the UICollectionView without scrolling. This is because I have put the UICollectionView in a UIScrollView, together with other UI Elements.
I have set the constraints in the interface builder, and I resize the UICollectionView on viewDidLoad, when I know how many items should be displayed. I do this with
[self allBooksCollectionViewSetConstraints];
I have set
[allBooksCollectionView setTranslatesAutoresizingMaskIntoConstraints:NO];
This is my code
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation) fromInterfaceOrientation {
[self allBooksCollectionViewConstraints];
}
-(NSInteger)allBooksCollectionViewHeight
{
float booksPerRow;
if (UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation))
{
booksPerRow = 6.0;
}
if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation))
{
booksPerRow = 8.0;
}
//calculate right height do display all cells
NSInteger cvHeight = (ceil((float)[allBooksCollectionView numberOfItemsInSection:0]/booksPerRow)*200.0)+49.0;
return cvHeight;
}
-(void)allBooksCollectionViewSetConstraints
{
NSInteger cvHeight = [self allBooksCollectionViewHeight];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"V:[view(%d)]", cvHeight] options:0 metrics:nil views:#{#"view":allBooksCollectionView}]];
}
I have tried removing the UICollectionView constraints from the UIScrollview, but it doesn't change a thing.
[scrollView removeConstraints:allBooksCollectionView.constraints];
On orientation change I get the following error:
Unable to simultaniously satisfy constraints ... (
"<NSLayoutConstraint:0x9aa49f0 V:[UICollectionView:0xc0b6000(849)]>",
"<NSLayoutConstraint:0xb2b15d0 V:[UICollectionView:0xc0b6000(1049)]>"
)
Will attempt to recover by breaking constraint <NSLayoutConstraint:0xb2b15d0 V:[UICollectionView:0xc0b6000(1049)]>
But the other constraint needs to be broken! Not this one, because 1049 is cvHeight.
How can I fix this?
I have tried removing the UICollectionView constraints from the UIScrollview, but it doesn't change a thing.
[scrollView removeConstraints:allBooksCollectionView.constraints];
This line of code is wrong. None of the constraints returned from collectionView.constraints will be on the scrollview, so this call will do nothing. You should store the constraints you care about in a property or instance variable:
if (collectionViewHeightConstraints)
{
[scrollView removeConstraints:collectionViewHeightConstraints];
}
collectionViewHeightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"V:[view(%d)]", cvHeight] options:0 metrics:nil views:#{#"view":allBooksCollectionView}];
[scrollView addConstraints:collectionViewHeightConstraints];
I made a few UIScrollView's in different views, they all worked without Autolayout.
I turned Autolayout on, because it was better for my app.
But since then, there's a big problem with my UIScrollView's:
No one is scrolling, they don't work.
Here's my code for a UIScrollView:
.m:
-(viewDidLoad) {
scrollerHome.contentSize = CGSizeMake(320, 1000);
scrollerHome.scrollEnabled = YES;
[self.view addSubview:scrollerHome];
scrollerHome.showsHorizontalScrollIndicator = false;
scrollerHome.showsVerticalScrollIndicator = false;
[super viewDidLoad];
}
.h:
#interface ViewController : UIViewController{
IBOutlet UIScrollView *scrollerHome;
}
Do I have to add some code because I turned on Autolayout?
You should call [super viewDidLoad] before doing anything !
In autolayout, you do not set the contentSize manually. Autolayout works slightly differently with scrollviews, whereby the contentSize of the scroll view is dictated by the constraints of the scrollview's subviews.
If you're trying to force the contentSize to some large size (for example, you're implementing some infinite scroller), you can just add a subview of the appropriate size, e.g.:
UIView *containerView = [[UIView alloc] init];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:containerView];
NSDictionary *views = NSDictionaryOfVariableBindings(containerView);
[self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[containerView]|" options:0 metrics:nil views:views]];
[self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[containerView(1000)]|" options:0 metrics:nil views:views]];
But if you were trying to set the contentSize in anticipation of adding subviews, you generally don't have to do anything, such as the above snippet. Just add your subviews, provide their constraints, and autolayout will adjust the scroll view's contentSize automatically.
As mentioned above, with autolayout, you can just add the subviews to your scrollview (with their constraints), and the contentSize will be calculated automatically for you.
There is a trick here, though. You sometimes you want to size a subview based upon the dimensions of the screen. But the usual technique of using the | symbols won't work. For example, for an imageview1 inside a scrollview, the usual #"H:|[imageview1]|" won't set the imageview1 to be the width of the screen, but rather it will define the scroll view's contentSize to match the width of imageview1, but it says nothing about what the width of that image view should be!
So, it's useful to capture a reference to the scroll view's superview. That way, you can use something like #"H:|[imageview1(==superview)]|", which not only says "make the scroll view's contentSize equal to the width of imageview1", but also "define the width of imageview1 to be equal to the width of the scroll view's superview."
Thus, for example, to add three images in a paging scroll view, you might do something like:
UIImageView *imageview1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"_DSC0004.jpg"]];
imageview1.contentMode = UIViewContentModeScaleAspectFit;
imageview1.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:imageview1];
UIImageView *imageview2 = ... // configured similar to imageview1
UIImageView *imageview3 = ... // configured similar to imageview1
UIView *superview = self.scrollView.superview;
NSDictionary *views = NSDictionaryOfVariableBindings(imageview1, imageview2, imageview3, superview);
// not only define the image view's relation with their immediate scroll view,
// but also explicitly set the size in relation to the superview, too!
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[imageview1(==superview)][imageview2(==superview)][imageview3(==superview)]|" options:0 metrics:nil views:views]];
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageview1(==superview)]|" options:0 metrics:nil views:views]];
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageview2(==superview)]|" options:0 metrics:nil views:views]];
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageview3(==superview)]|" options:0 metrics:nil views:views]];
self.scrollView.pagingEnabled = YES;
From the Apple iOS 6.0 release notes:
"In general, Auto Layout considers the top, left, bottom, and right edges of a view to be the visible edges. That is, if you pin a view to the left edge of its superview, you’re really pinning it to the minimum x-value of the superview’s bounds. Changing the bounds origin of the superview does not change the position of the view.
The UIScrollView class scrolls its content by changing the origin of its bounds. To make this work with Auto Layout, the top, left, bottom, and right edges within a scroll view now mean the edges of its content view."
You can find the full notes here and find the answer to your question in the section that I quoted from. They give code examples on how to use UIScrollView in a mixed Auto Layout environment.
I have a fairly simple view configuration:
A UIViewController, with a child UIScrollView and a UIImageView in this UIScrollView.
I set the UIImageView with a height sufficient to break out of the visible area (ie. higher to 1024pt), and set the Bottom space to superview constraint of my UIImageView to a fixed positive value (20 for example).
The whole setup works as expected, the image scrolls nicely in its parent.
Except when the view is scrolled (the effect is more visible if you scrolled to the bottom of the view), then disappear, and appear again (you switched to another view and came back) the scrolling value is restored, but the content of the scroll view is moved to the outside top part of its parent view.
This is not simple to explain, I'll try to draw it:
If you want to test/view the source (or the storyboard, I did not edit a single line of code). I put a little demo on my github: https://github.com/guillaume-algis/iOSAutoLayoutScrollView
I did read the iOS 6 changelog and the explanation on this particular topic, and think this is the correct implementation of the second option (pure auto layout), but in this case why is the UIScrollView behaving so erratically ? Am I missing something ?
EDIT: This is the exact same issue as #12580434 uiscrollview-autolayout-issue. The answers are just workarounds, as anyone found a proper way to fix this or is this a iOS bug ?
EDIT 2: I found another workaround, which keep the scroll position in the same state the user left it (this is an improvement over 12580434's accepted answer):
#interface GAViewController ()
#property CGPoint tempContentOffset;
#end
#implementation GAViewController
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.tempContentOffset = self.mainScrollView.contentOffset;
self.scrollView.contentOffset = CGPointZero;
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.scrollView.contentOffset = self.tempContentOffset;
}
This basically save the offset in viewWillAppear, reset it to the origin, and then restore the value in viewDidAppear. The problem seems to occur between these two calls, but I can't find its origin.
Yeah, something strange happened with UIScrollView in pure autolayout environment. Re-reading the iOS SDK 6.0 release notes for the twentieth time I found that:
Note that you can make a subview of the scroll view appear to float (not scroll) over the other scrolling content by creating constraints between the view and a view outside the scroll view’s subtree, such as the scroll view’s superview.
Solution
Connect your subview to the outer view. In another words, to the view in which scrollview is embedded.
As IB does not allow us set up constraints between the imageView and a view outside the scroll view’s subtree, such as the scroll view’s superview then I've done it in code.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.view removeConstraints:[self.view constraints]];
[self.scrollView removeConstraints:[self.scrollView constraints]];
[self.imageView removeConstraints:[self.imageView constraints]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[_scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_scrollView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_scrollView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[_imageView(700)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_imageView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_imageView(1500)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_imageView)]];
}
And vau! It works!
The edit didn't work for me. But this worked:
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.tempContentOffset = self.scrollView.contentOffset;
self.scrollView.contentOffset = CGPointZero;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.scrollView.contentOffset = self.tempContentOffset;
}
For me I went to the IB clicked my view controller that contains the scroll view. Then I went to Attribute Inspector -> View Controller -> Extend Edges -> Uncheck "Under Top Bars" and "Under Bottom Bars".
Simple solution found, Just put
[self setAutomaticallyAdjustsScrollViewInsets:NO];
in your ViewControllers viewDidLoad method
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[self setAutomaticallyAdjustsScrollViewInsets:NO];
}
I had a similar problem using a UIScrollView in a UIViewController with a top extra space that was not visible in Storyboard. The solution for me was to uncheck the "Adjust Scroll View Insets" on the ViewController storyboard properties : see answer Extra Space (inset) in Scroll View at run time
Add a global property contentOffset and save the current contentOffset in viewDidDisappear.
Once you return the method viewDidLayoutSubviews will be called and you can set your original contentOffset.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.scrollView setContentOffset:self.contentOffset animated:FALSE];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
self.contentOffset = self.scrollView.contentOffset;
[self.scrollView setContentOffset:CGPointMake(0, 0) animated:FALSE];
}
Looks like the problem solved with the dispatch_async during the viewWillAppear:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGPoint originalContentOffset = self.scrollView.contentOffset;
self.scrollView.contentOffset = CGPointZero;
dispatch_async(dispatch_get_main_queue(), ^{
self.scrollView.contentOffset = originalContentOffset;
});
}
I am configuring a custom UITableViewCell using a prototype cell in a Storyboard. However, all the UILabels (and other UI elements) do not seem to be added to the cell's contentView, instead being added to the UITableViewCell view directly. This creates issues when the cell is put into editing mode, as the content is not automatically shifted/indented (which it would do, if they were inside the contentView).
Is there any way to add the UI elements to the contentView when laying out the cell using Interface Builder/Storyboard/prototype cells? The only way I have found is to create everything in code and use [cell.contentView addSubView:labelOne] which wouldn't be great, as it is much easier to layout the cell graphically.
On further investigation (viewing the subview hierarchy of the cell) Interface Builder does place subviews within the cell's contentView, it just doesn't look like it.
The root cause of the issue was iOS 6 autolayout. When the cell is placed into editing mode (and indented) the contentView is also indented, so it stands to reason that all subviews within the contentView will move (indent) by virtue of being within the contentView. However, all the autolayout constraints applied by Interface Builder seem to be relative to the UITableViewCell itself, rather than the contentView. This means that even though the contentView indents, the subviews contained within do not - the constraints take charge.
For example, when I placed a UILabel into the cell (and positioned it 10 points from the left-hand side of the cell) IB automatically applied a constraint "Horizontal Space (10)". However, this constraint is relative to the UITableViewCell NOT the contentView. This means that when the cell is indented, and the contentView moves, the label stays put as it is complying with the constraint to remain 10 points from the left-hand side of the UITableViewCell.
Unfortunately (as far as I am aware) there is no way to remove these IB created constraints from within IB itself, so here is how I solved the problem.
Within the UITableViewCell subclass for the cell, I created an IBOutlet for that constraint called cellLabelHSpaceConstraint. You also need an IBOutlet for the label itself, which I called cellLabel. I then implemented the -awakeFromNib method as per below:
- (void)awakeFromNib {
// -------------------------------------------------------------------
// We need to create our own constraint which is effective against the
// contentView, so the UI elements indent when the cell is put into
// editing mode
// -------------------------------------------------------------------
// Remove the IB added horizontal constraint, as that's effective
// against the cell not the contentView
[self removeConstraint:self.cellLabelHSpaceConstraint];
// Create a dictionary to represent the view being positioned
NSDictionary *labelViewDictionary = NSDictionaryOfVariableBindings(_cellLabel);
// Create the new constraint
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-10-[_cellLabel]" options:0 metrics:nil views:labelViewDictionary];
// Add the constraint against the contentView
[self.contentView addConstraints:constraints];
}
In summary, the above will remove the horizontal spacing constraint which IB automatically added (as is effective against the UITableViewCell rather than the contentView) and we then define and add our own constraint to the contentView.
In my case, all the other UILabels in the cell were positioned based upon the position of the cellLabel so when I fixed up the constraint/positioning of this element all the others followed suit and positioned correctly. However, if you have a more complex layout then you may need to do this for other subviews as well.
As mentioned, XCode's Interface Builder is hiding the UITableViewCell's contentView. In reality, all UI elements added to the UITableViewCell are in fact subviews of the contentView.
For the moment, it IB is not doing the same magic for layout constraints, meaning that they are all expressed at UITableViewCell level.
A workaround is in a subclass's awakeFromNib to move all NSAutoLayoutConstrains from UITableViewCell to it's contentView and express them in terms of the contentView :
-(void)awakeFromNib{
[super awakeFromNib];
for(NSLayoutConstraint *cellConstraint in self.constraints){
[self removeConstraint:cellConstraint];
id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
NSLayoutConstraint* contentViewConstraint =
[NSLayoutConstraint constraintWithItem:firstItem
attribute:cellConstraint.firstAttribute
relatedBy:cellConstraint.relation
toItem:seccondItem
attribute:cellConstraint.secondAttribute
multiplier:cellConstraint.multiplier
constant:cellConstraint.constant];
[self.contentView addConstraint:contentViewConstraint];
}
}
Here is a subclass, based on other answers ideas, I'm going to base my custom cells on:
#interface FixedTableViewCell ()
- (void)initFixedTableViewCell;
#end
#interface FixedTableViewCell : UITableViewCell
#end
#implementation FixedTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (nil != (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) {
[self initFixedTableViewCell];
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self initFixedTableViewCell];
}
- (void)initFixedTableViewCell {
for (NSInteger i = self.constraints.count - 1; i >= 0; i--) {
NSLayoutConstraint *constraint = [self.constraints objectAtIndex:i];
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
BOOL shouldMoveToContentView = YES;
if ([firstItem isDescendantOfView:self.contentView]) {
if (NO == [secondItem isDescendantOfView:self.contentView]) {
secondItem = self.contentView;
}
}
else if ([secondItem isDescendantOfView:self.contentView]) {
if (NO == [firstItem isDescendantOfView:self.contentView]) {
firstItem = self.contentView;
}
}
else {
shouldMoveToContentView = NO;
}
if (shouldMoveToContentView) {
[self removeConstraint:constraint];
NSLayoutConstraint *contentViewConstraint = [NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant];
[self.contentView addConstraint:contentViewConstraint];
}
}
}
#end
An alternative to subclassing is to revise the constraints in cellForRowAtIndexPath.
Embed all the content of the cell inside a container view. Then point the leading and trailing constraints to the cell.contentView rather than the table view cell.
UIView *containerView = [cell viewWithTag:999];
UIView *contentView = [cell contentView];
//remove existing leading and trailing constraints
for(NSLayoutConstraint *c in [cell constraints]){
if(c.firstItem==containerView && (c.firstAttribute==NSLayoutAttributeLeading || c.firstAttribute==NSLayoutAttributeTrailing)){
[cell removeConstraint:c];
}
}
NSLayoutConstraint *trailing = [NSLayoutConstraint
constraintWithItem:containerView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:contentView
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:0];
NSLayoutConstraint *leading = [NSLayoutConstraint
constraintWithItem:containerView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:contentView
attribute:NSLayoutAttributeLeading
multiplier:1
constant:0];
[cell addConstraint:trailing];
[cell addConstraint:leading];
I think this is fixed in iOS 7 beta 3 making the workarounds unnecessary from that point on (but probably harmless as in most cases they will become empty operations).
Based on the code by Skoota (I am a beginner, don't know much of what you did, but excellent work) my suggestion is to put all your stuff in an edge-to-edge container view and add the following:
In the cell's header file, I have the following IBOutlets:
#property (weak, nonatomic) IBOutlet UIView *container;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *leftConstrain;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *rightConstrain;
In the implementation file, I have the following in awakeFromNib:
// Remove the IB added horizontal constraint, as that's effective gainst the cell not the contentView
[self removeConstraint:self.leftConstrain];
[self removeConstraint:self.rightConstrain];
// Create a dictionary to represent the view being positioned
NSDictionary *containerViewDictionary = NSDictionaryOfVariableBindings(_container);
// Create the new left constraint (0 spacing because of the edge-to-edge view 'container')
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-0-[_container]" options:0 metrics:nil views:containerViewDictionary];
// Add the left constraint against the contentView
[self.contentView addConstraints:constraints];
// Create the new constraint right (will fix the 'Delete' button as well)
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"[_container]-0-|" options:0 metrics:nil views:containerViewDictionary];
// Add the right constraint against the contentView
[self.contentView addConstraints:constraints];
Again, the above was made possible by Skoota. Thanks!!! Al credits go to him.