Autolayout constraints: Unable to simultaneously satisfy constraints - ios

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];

Related

Why visual constraints not supporting UITableViewAutomaticDimension?

When i tried with constraints via storyboard its working perfectly .
this is my view controller.
When i was tried with visual constraints i am getting screen like this .
this is my visual constraints :
NSDictionary *viewsdictionary=#{#"txtview":cell.txtview,
#"lbldate":cell.lbldate};
[cell.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[txtview]-|" options:0 metrics:nil views:viewsdictionary]];
[cell.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[lbldate]-|" options:0 metrics:nil views:viewsdictionary]];
[cell.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[txtview]-10-[lbldate(200#1000)]-|" options:0 metrics:nil views:viewsdictionary]];
if([from isEqualToString:_tophonenumber])
{
cell.txtview.layer.borderWidth=2.0;
cell.txtview.layer.borderColor=[UIColor yellowColor].CGColor;
[cell.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[lbldate]-10-[txtview]-|" options:0 metrics:nil views:viewsdictionary]];
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:YES];
self.tableview.estimatedRowHeight = 160;
self.tableview.rowHeight = UITableViewAutomaticDimension;
}
for some reason i am changing the txtview and label but thats also not changing what i did wrong ?
Help me to over come this problem thanks :)
In your IB constraints, I assume that your date has a fixed width, while your text box has a variable width and is just asked to stick to the date leading edge, with the date only adhering from trailing edge to superview trailing? In your programmatic constraints, you do not give that date the fixed width unless a string if statement is satisfied, right? So otherwise, the date has not got the constraints it needs.
Also though, do you call [cell layoutIfNeeded] after updating the constraints? They might need that as well, if you update the constraints on cellForRowAtIndexPath. Theres also a final thing you might need to do with the UITableView so that the table is updated :
[self.tableView beginUpdates];
[self.tableView endUpdates];
If you call this after updating cell constraints, it ensures that the tableview cells are all re-aligned correctly (particular if the height has changed). But I wouldnt call that after each cell has changed - do it once when the tableview has finished updating.

Auto layout error Unable to simultaneously satisfy constraints with UIScrollView

I have a UIScrollView that is to show UIImageViews. The ImageViews are programmatically inserted at runtime based on how many images are saved by the user previously. I get the error below:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want. Try this: (1) look at each constraint and try to
figure out which you don't expect; (2) find the code that added the
unwanted constraint or constraints and fix it. (Note: If you're seeing
NSAutoresizingMaskLayoutConstraints that you don't understand, refer
to the documentation for the UIView property
translatesAutoresizingMaskIntoConstraints)
"<NSLayoutConstraint:0x17029cd90 H:[UIView:0x15cd31e70]-(0)-| (Names: '|':UIScrollView:0x15cd326b0 )>",
"<NSLayoutConstraint:0x17029d060 UIScrollView:0x15cd326b0.trailingMargin == UIView:0x15cd31e70.trailing>"
I do the basic Autolayout thing where the scrollview is pinned to all four sides at 0 points. I then add a contentView as a subview (plain UIView) of the UIScrollView which is also pinned to all four sides at 0 points.
EDIT Storyboard constraints image
I give the contentView a width in code like so:
CGSize pagesScrollViewSize = self.scrollView.frame.size;
NSDictionary *views = #{ #"contentView" : self.contentView};
NSDictionary *metrics = #{ #"width" : #(pagesScrollViewSize.width * self.mutableArray.count) };
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[contentView(width)]-|" options:0 metrics:metrics views:views];
[self.scrollView addConstraints:constraints];
[self loadVisiblePages];
The UIImageViews are added like so where UIImageViews are added based on the number of pages set when the segue to the ViewController occurs.
-(void)loadVisiblePages{
CGRect frame = self.scrollView.bounds;
frame.origin.x = frame.size.width * self.page;
frame.origin.y = 0.0f;
ImageForArchiving *newImageObject = (ImageForArchiving*)self.mutableArray[page];
UIImage *imageForNewPageView = newImageObject.image;
UIImageView *newPageView = [[UIImageView alloc] initWithFrame:frame];
[newPageView setTranslatesAutoresizingMaskIntoConstraints:YES];
newPageView.contentMode = UIViewContentModeScaleAspectFit;
newPageView.image = imageForNewPageView;
[self.scrollView addSubview:newPageView];
[self.pageViews replaceObjectAtIndex:page withObject:newPageView];
}
}
Additionally, when I scroll the UIScrollView the images displayed change size erratically on rotation. I think that this is just a consequence of the above warning and the fact that I haven't layed out the UIImageViews yet. What does the above warning mean in this context and how do I fix it?
It seems you have pinned trailingMargin of scrollView to contentView.trailing.
Change scrollView.Trailing Margin to scrollView.Trailing for this constraint
You can do this in the activity inspector in the storyboard after selecting your constraint.
Alternatively, clear all constraints on your contentView. Then while adding pinning constraints again uncheck Constrain to margins and set all constants 0.
AND
Change this line in your code
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[contentView(width)]-|" options:0 metrics:metrics views:views];
[self.scrollView addConstraints:constraints];
with this:
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[contentView(width)]|" options:0 metrics:metrics views:views];
[self.scrollView addConstraints:constraints];
Using #"H:|-[contentView(width)]-|" in visual format means pinning your contentView to the superView's margins and adds a space of 8 pts between the superView and subView. In your storyboard constraints you had set up constraints with the Trailing and Leading edges of the UIScrollView, while in the programmatically added constraint you had used Trailing Margin and Leading Margin (kind of asking the contentView to maintain an 8 pt. padding). Hence, the conflict.
Check Visual Format syntax here.

Dynamic UILabel size iOS 7 issue

I'm trying resize a label dynamically according to text height. The height can vary from 0 to many lines in the UILabel. I've come up with a solution for this problem that works fine on iOS 8 but fails on iOS 7.1 which I'm trying to support as well.
Autolayout is not being used in this project and all constraints are done programatically.
The code is as follows:
//TableDelegate.m
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 85.0f;
}
//CustomTableViewCell.m
-(UILabel *)commentTextLabel
{
if(!_commentTextLabel)
{
_commentTextLabel = [UILabel new];
_commentTextLabel.numberOfLines = 0;
_commentTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
}
return _commentTextLabel;
}
-(void)setupViews
{
[self.contentView addSubview:self.profilePictureView];
[self.contentView addSubview:self.userName];
[self.contentView addSubview:self.timePublishedLabel];
[self.contentView addSubview:self.commentTextLabel];
[self.contentView addSubview:self.seeMoreButton];
self.backgroundColor = [UIColor salooteInputTextBg];
self.contentView.backgroundColor = [UIColor salooteInputTextBg];
NSDictionary *views = #
{
#"picture" : self.profilePictureView,
#"userName" : self.userName,
#"timePublished" : self.timePublishedLabel,
#"text" : self.commentTextLabel,
#"seeMore" : self.seeMoreButton
};
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[picture(38)]-5-[userName]-5-[timePublished]-5-|" options:0 metrics:nil views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[picture]-5-[text]-5-|" options:0 metrics:nil views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[seeMore]-5-|" options:0 metrics:nil views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-5-[userName]-5-[text]-5-[seeMore]-5-|" options:0 metrics:nil views:views]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-5-[picture(38)]" options:0 metrics:nil views:views]];
}
-(void)updateConstraints
{
[super updateConstraints];
}
iOS 8 result (left) iOS 7.1 result (right)
I'm not setting any height constraint in my code for the UILabel but rather trying to let the constraints adjust the vertical height for me. If anyone has some input on how to make this work properly on iOS 7.1 I would really appreciate it.
Moving constraints into setupViews produces this: (iOS 7.1 top iOS 8 bottom)
It seems to me you're not adding vertical constraints to the commentTextLabel? You only have this:
//Comment text
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[picture]-5-[text]-0-|" options:0 metrics:nil views:views]];
Try setting a vertical constraint as well--it's likely that you're getting insufficient constraints errors and iOS 8 is guessing the height better than iOS 7. Also, if you're adding constraints to the views, you shouldn't have to call sizeToFit inside the getter.
Autolayout is not being used in this project and all constraints are done programatically.
You're still using Autolayout even if you're adding the constraints only programatically. :)
In response to edits
Your vertical height constraint is insufficient--you only specified the height of the commentTextLabel but not its y-coordinate. Remember that the main objective in Autolayout is to provide a complete set of constraints such that iOS can compute for a view's x, y, width, and height.
I think your constraints are screwed up overall. :) Try adding these rules to the content view instead (I just used 5 for any padding):
H:|-5-[picture(38)]-5-[username]-5-[timePublished]-5-|
H:[picture]-5-[text]-5-|
H:|-5-[seeMore]-5-|
V:|-5-[username]-5-[text]-5-[seeMore]-5-|
V:|-5-[picture(38)]
Also, add your constraints in setupViews--you should only have to add your constraints once and ONLY modify them in updateConstraints. I think updateConstraints is called every time layoutSubviews is called so your constraints keep getting added every time the cell's layout is refreshed.
In response to edits
Your label's word wrap style must be set, too. From inside the commentTextLabel, add
_commentTextLabel.lineBreakMode = NSLineBreakByWordWrapping;
Always set that in conjunction to numberOfLines = 0 if you want a UILabel with a dynamic height.
You also need to right-align your seeMore label (it occupies the full width of the cell minus the padding) by setting that label's alignment property.
And try providing a bigger faux height for now--perhaps 150 or 200 instead of 85, just so we can see all the elements.
For the timePublished label, I forgot to indicate the following vertical constraint:
V:|-5-[timePublished]
I have found that the only way to support both iOS7 and iOS8 easily, is to do the height calculations for each cell yourself using off screen prototypes. The following is an excellent article on the issues. I could find no way to mix auto layout height calculation from iOS8 with manual height estimates for iOS7 in a single code base.
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
The only issue I had with this method was when I used size classes to change the cell font sizes so I could have larger font on iPad etc... This issue is discussed here:
Offscreen UITableViewCells (for size calculations) not respecting size class?

UIScrollView doesn't work with Autolayout (iOS 6)

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.

UIScrollView Zoom Does Not Work With Autolayout

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.

Resources