Consider a UIScrollView
with a single subview. The subview is an UIImageView
with the following size constraints:
- Its height must be equal to the height of the
UIScrollView
. - Its width must be the width of the image scaled proportionally to the height of the
UIImageView
.
It is expected that the width of the UIImageView
will be bigger than the width of the UIScrollView, hence the need for scrolling.
The image might be set during viewDidLoad
(if cached) or asynchronously.
How do you implement the above using autolayout, making as much use as possible of Interface builder?
What I've done so far
Based on this answer I configured my nib like this:
- The
UIScrollView
is pinned to the edges of its superview. - The
UIImageView
is pinned to the edges of theUIScrollView
. - The
UIImageView
has a placeholder intrinsic size (to avoid the Scrollable Content Size Ambiguity error)
As expected, the result is that the UIImageView
is sized to the size of the UIImage
, and the UIScrollView
scrolls horizontally and vertically (as the image is bigger than the UIScrollView
).
Then I tried various things which didn't work:
- After loading the image manually set the frame of
UIImageView
. - Add a constraint for the width of the UIImageView and modify its value after the image has been loaded. This makes the image even bigger (?!).
- Set
zoomScale
after the image is loaded. Has no visible effect.
Without autolayout
The following code does exactly as I want, albeit without autolayout or interface builder.
- (void)viewDidLoad
{
{
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:scrollView];
self.scrollView = scrollView;
}
{
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width, self.scrollView.frame.size.height)];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.scrollView addSubview:imageView];
self.scrollView.contentSize = imageView.frame.size;
self.imageView = imageView;
}
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self layoutStripImageView];
}
- (void)layoutStripImageView
{ // Also called when the image finishes loading
UIImage *image = self.imageView.image;
if (! image) return;
const CGSize imageSize = image.size;
const CGFloat vh = self.scrollView.frame.size.height;
const CGFloat scale = vh / imageSize.height;
const CGFloat vw = imageSize.width * scale;
CGSize imageViewSize = CGSizeMake(vw, vh);
self.imageView.frame = CGRectMake(0, 0, imageViewSize.width, imageViewSize.height);
self.scrollView.contentSize = imageViewSize;
}
I'm trying really hard to move to autolayout but it's not being easy.
Under the autolayout regime, ideally the UIScrollView contentSize is solely determined by the constraints and not set explicitly in code.
So in your case:
Create constraints to pin the subview to the
UIScrollView
. The constraints have to ensure the margin between the subview and the scroll view are 0. I see that you have already tried this.Create a height and a width constraint for your subview. Otherwise, the intrinsic size of the
UIImageView
determines its height and width. At design time, this size is only a placeholder to keep Interface Builder happy. At run time, it will be set to the actual image size, but this is not what you want.During
viewDidLayoutSubviews
, update the constraints to be actual content size. You can either do this directly by changing theconstant
property of the height and width constraint, or callingsetNeedsUpdateConstraints
and overridingupdateConstraints
to do the same.This ensures that the system can derive
contentSize
solely from constraints.I've done the above and it works reliably on iOS 6 and 7 with a
UIScrollView
and a custom subview, so it should work forUIImageView
too. In particular if you don't pin the subview to the scroll view, zooming will be jittery in iOS 6.You may also try creating height and width constraints that directly reference a multiple of the height and width of the scroll view, but I haven't tried this other approach.
translatesAutoresizingMaskIntoConstraints
is only required when you instantiate the view in the code. If you instantiate it in the IB it's disabled by defaultIn my opinion the
UIImageView
should fill theScrollView
. Later I'd try setting the zoom of thescrollview
to the value that suits you well so the image can only be panned in one directionIn my case it was a full width UIImageView the had a defined height constraint that causing the problem.
I set another constraint on the UIImageView for the width that matched the width of the UIScrollView as it is in interface builder then added an outlet to the UIViewController:
then on viewDidLayoutSubviews I updated the constraint:
This seemed to do the trick.