I have an autolayout xib with constraints added in my application.
Then I add my xib view to a scrollView lateral like a pageController.
But when I add this to the scrollView I get a lot of error with constraints and the outlets isn't showed properly. "[LayoutConstraints] Unable to simultaneously satisfy constraints."
This is my xib class code:
#implementation InvitView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
nibname=#"Invit";
[self addSubview:
[[[NSBundle mainBundle] loadNibNamed:nibname owner:self options:nil] objectAtIndex:0]];
}
return self;
}
#end
And I add my xib to the scrollView with this code:
- (void)viewDidLoad {
[super viewDidLoad];
float width = self.scroll.frame.size.width;
float height = self.scroll.frame.size.height;
int count = 0;
InvitView *myView = [[InvitView alloc] initWithFrame:CGRectMake(self.scroll.frame.origin.x, self.scroll.frame.origin.y, self.scroll.frame.size.width, self.scroll.frame.size.height)];
[myView setFrame:CGRectMake(width*count++, 0, width, height)];
[myView setNeedsUpdateConstraints];
[self.scroll addSubview:myView];
}
I'm trying set translatesAutoresizingMaskIntoConstraints = NO; but with this code I lost all constraints, and I want keep this then add to the scrollView.
How can I keep my initial constraints in the xib?
EDIT
On the one hand, I have this constraints to my UIScrollView in my viewwController:
On the another hand I have this in my xib:
And the exactly log is:
I have a xib with constraints, and I have a viewController in my autolayout storyboard with a scrollView where I added the xib.
My xib have constraints, but when I add the xib to the autolayout storyboard the outlets change position and size, it is losing his constraints...
I use this function to create the xib :
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
nibname=#"myViewXib";
[self addSubview:
[[[NSBundle mainBundle] loadNibNamed:#"myViewXib" owner:self options:nil] objectAtIndex:0]];
[self setNeedsLayout];
}
return self;
}
And this is the function to load the xib into my storyboard in the viewDidLoad:
MyViewXib *myView = [[MyViewXib alloc] initWithFrame:CGRectMake(self.scroll.frame.origin.x, self.scroll.frame.origin.y, self.scroll.frame.size.width, self.scroll.frame.size.height)];
[myView setFrame:CGRectMake(width*count++, 0, width, height)];
[self.scroll addSubview:myView];
[self.scroll layoutSubviews];
What is wrong in my code? Why are the constraints not working?
thanks!
Instead of putting add subview code in the viewDidLoad method you can try it by putting it in viewDidAppear method since the view controller don't have the exact frame of any view in viewDidLoad method.
I'm running into a problem setting a tableHeaderView for a UITableView. I would like to have a detailsView be set as the tableHeaderView. The height of this detailsView will vary slightly and is not known immediately in view did load. The detailsView also has it's own subViews that have their own auto layout constraints. All auto layout is being done programatically. Let me post some sample code:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view addSubview:self.tableView];
self.tableView.tableHeaderView = self.detailsView;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self loadDetails];
}
-(void)loadDetails
{
//Omitted but does the following:
//1. Makes a call to the api to get details
//2. Once received sets the details to the detailsView
//3. Details could vary which influences detailView height size.
}
- (DetailsView *)detailsView
{
if(!_detailsView)
{
__weak DetailedViewController *_self = self;
_detailsView = [DetailsView new];
_detailsView.backgroundColor = [UIColor greenColor];
_detailsView.translatesAutoresizingMaskIntoConstraints = NO;
}
return _detailsView;
}
-(void)updateViewConstraints
{
NSDictionary *views = #{
#"table" : self.tableView,
};
//Comment Detail View
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[table]-0-|" options:0 metrics:0 views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[table]-0-|" options:0 metrics:0 views:views]];
}
The problem with this approach is the tableHeaderView is pushed up to the top, meaning I can't scroll all the way through it properly. I'm not sure why this is happening. What I did as a test was replaced the detailsView with a UIImageView as follows.
-(UIImageView *)testImageView
{
if(!_testImageView)
{
NSString *filePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"blackImage"] ofType:#"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
_testImageView = [[UIImageView alloc]initWithImage:image];
_testImageView.translatesAutoresizingMaskIntoConstraints = NO;
}
return _testImageView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.view addSubview:self.tableView];
self.tableView.tableHeaderView = self.testImageView;
}
This code produces exactly what I need without knowing the height or any of the dimensions of the image.
As evidence by the photo, the header view is fully viewable and scrollable and I did not need to know any size of the image for this to work. I'd like to achieve the same thing with a UIView instead of the UIImageView for the tableHeaderView.
Important Notes:
The details view is not added as a sub view
The details view has not vertical height constraints or any constraints computed
The details view sub views have constraints computed programatically
Things I've Researched:
I've looked into instrinsicSize, sizeThatFits, anything that would allow a UIView to fill up the parent container view (tableHeaderView). I've tried various combinations of things with no success.
If anyone has a solution for how to solve this problem I'd appreciate it greatly!
(Let me know if this is not enough code to convey the context of the problem and I will post more.)
Use
systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
0) Init your header generically, being sure that the constraints hit all four sides of the headerView's frame.
1) Set the frame of the header view using
systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
2) Assign your header view to the tableview's frame
For example:
self.headerView = [[YourCustomHeaderView alloc] initWithYourCustomObject:obj];
self.headerView.frame = CGRectMake(0,0, SCREEN_WIDTH, [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height);
self.tableView.tableHeaderView = self.headerView;
I've got a segmentedControl with three views in my app, one of which is a scrollView which works like a sort of gallery without zoom, with pageControl and an imageView at the center.
The hierarchy is like
--> Segmented Control (3 views) : descriptionView, imageTabView, shareView
----> imagesTabView (UIView)
------> scrollView
------> imageView
----> pageControl
When the device is portrait or landscape, the imageView images are shown correctly, they're centered and scrolling works perfectly fine.
The only problem is that when you turn the device again, if the image is "in the middle" (e.g. is the 2nd of 3 or the 3rd of 6), it's being shown decentered, far left or right, and with a little swipe it goes back at the center, while if the image is the first or the last one, it works properly.
I've looked here on S.O. on various threads, tried to set a contentView as a subview of the scrollView and add the imageView as subview of contentView, but didn't work, tried to attach the imageView to the bottom or the right of the scrollView but didn't work either.
I feel like I'm a step away to achieve what I want to do, the only problem is that I can't get why it's not centered.
In viewWillLayoutSubviews I've specified the contentSize, in order that when it rotates, the size it's set correctly, like
-(void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
self.scrollView.contentSize = CGSizeMake (self.scrollView.frame.size.width * photosArray.count, 1);
}
Here's how I'm initializing the pageControl, the scrollView and the imageView:
-(void)configureImageTab{
pageControl = [UIPageControl new];
[pageControl addTarget:self action:#selector(changePage) forControlEvents:UIControlEventValueChanged];
pageControl.translatesAutoresizingMaskIntoConstraints = NO;
//Don't show pageControl when there are no photos
if (photosURL.count == 0)
pageControl.hidden = YES;
//Configuring scrollView
self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.imageSegmentView.frame.size.width, self.imageSegmentView.frame.size.height-pageControl.frame.size.height)];
self.scrollView.pagingEnabled = YES;
self.scrollView.delegate = self;
self.scrollView.userInteractionEnabled = YES;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
//... Code cut - adding remote images to fetch to array
//Actual setup -> scrollView adding imageView as subview with all the images
for (int i =0; i< photosArray.count; i++){
CGRect frame;
frame.origin.x = self.scrollView.frame.size.width * i;
frame.origin.y = 0;
frame.size = self.scrollView.frame.size;
//imageView setup
imageView = [[UIImageView alloc]initWithFrame:frame];
imageView.backgroundColor = [UIColor clearColor];
imageView.clipsToBounds = YES;
imageView.userInteractionEnabled = YES;
imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
imageView.translatesAutoresizingMaskIntoConstraints = YES;
imageView.contentMode = UIViewContentModeScaleAspectFit;
//Setting images urls
[imageView setImageWithURL:[NSURL URLWithString:[photosArray objectAtIndex:i]] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
//Error handling
}
}usingActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
//Adding gesture recognizer to scrollView and imageView as subview
[self.scrollView addGestureRecognizer:singleTap];
[self.scrollView addSubview:imageView];
}
//Setting the contentSize
pageControl.numberOfPages = [photosURL count];
[self.imageSegmentView addSubview:self.scrollView];
[self.imageSegmentView addSubview:pageControl];
//Constraints
NSDictionary *views = #{#"pageControl" : pageControl, #"scrollView" : self.scrollView};
[self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[pageControl]-0-|" options:0 metrics:nil views:views]];
[self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]-1-[pageControl]-1-|" options:0 metrics:nil views:views]];
[self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|" options:0 metrics:nil views:views]];
[pageControl addConstraint:[NSLayoutConstraint constraintWithItem:pageControl attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.imageSegmentView attribute:NSLayoutAttributeHeight multiplier:0 constant:30]];
}
#pragma mark - scrollView delegate -
-(void)scrollViewDidScroll:(UIScrollView *)sView{
CGFloat pageWidth = self.scrollView.frame.size.width;
int page = floor ((self.scrollView.contentOffset.x - pageWidth /2) /pageWidth) +1;
self.pageControl.currentPage = page;
}
-(IBAction)changePage {
CGRect frame;
frame.origin.x = self.scrollView.frame.size.width * self.pageControl.currentPage;
frame.origin.y = 0;
frame.size = self.scrollView.frame.size;
[self.scrollView scrollRectToVisible:frame animated:YES];
}
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
pageControlBeingUsed = NO;
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
pageControlBeingUsed = NO;
}
One note to make: imageView is using autoresizingMask: without that, it wouldn't be able to show the images properly.
My guess is that probably there's something to fix within the scrollView delegate, but I'm not quite sure.
Any suggestion appreciated!
EDIT
I've noticed that the same bug occurs in Twitter app when browsing a user's pictures and then turning the device.
EDIT 2 for TL;DR
Basically, let's say I have 3 images in an horizontal scrollView with paging.
I turn the device from Portrait to Landscape on the first photo, and it's shown at its own place, correctly centered.
I move to the next photo, shown centered, and then I turn the device again to Portrait. The photo is not aligned correctly, is not centered
Practically, the first and the last images, when the device rotates multiple times, are shown centered. The others are not centered
EDIT 3
I've extracted some of the lines and made a sample project to demonstrate the issue I'm having. I guess there's definitely something up with contentSize.
We can fix the specific bug you're talking about (scroll view not aligned to page boundary after rotation) by recording the current page when the interface is about to rotate, and then setting the scroll view's contentOffset appropriately during the rotation, after the system has updated the scroll view's bounds size. Let's add a pageNumberPriorToRotation instance variable:
#implementation ViewController {
CGFloat pageNumberPriorToRotation;
}
Then, we set it when the interface is about to rotate:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self setPageNumberPriorToRotation];
}
- (void)setPageNumberPriorToRotation {
CGRect bounds = self.scrollView.bounds;
static const int kNumberOfImages = 3;
pageNumberPriorToRotation = fmin(round(bounds.origin.x / bounds.size.width),
kNumberOfImages - 1);
}
and we use it to set the scroll view's contentOffset during the interface rotation:
-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self updateScrollViewLayout];
}
- (void)updateScrollViewLayout {
CGRect bounds = self.scrollView.bounds;
bounds.origin.x = bounds.size.width * pageNumberPriorToRotation;
self.scrollView.bounds = bounds;
}
This takes care of your primary complaint: the scroll view will always be aligned to a page view boundary after a rotation.
However…
There are some other problems with the scroll view interaction. In landscape orientation, I can't scroll to the third image. After rotating to landscape and back to portrait, I can scroll to a blank fourth page. These problems are presumably what you meant by “there's definitely something up with contentSize”.
Furthermore, your code has a number of problems. It uses some outdated style, like explicitly declaring instance variables for properties and putting instance variables in the header file. It also suffers from Massive View Controller. It could really stand to be rewritten in modern style, and using features like UITabBarController and UIPageViewController.
Anyway, you probably have neither the time nor the inclination to do that amount of work, so I will show you how to solve the contentSize problems and slim down your VC a little at the same time.
I'll make a UIScrollView subclass called ImageScrollView. You give me the array of images and I'll take care of setting up its subviews and aligning to a page boundary after a rotation. Here's my header file:
ImageScrollView.h
#import <UIKit/UIKit.h>
#interface ImageScrollView : UIScrollView
#property (nonatomic, copy) NSArray *images;
#end
To implement this, I'll need some instance variables:
ImageScrollView.m
#import "ImageScrollView.h"
#import <tgmath.h>
#implementation ImageScrollView {
NSMutableArray *imageSubviews;
CGSize priorSize;
CGFloat pageNumber;
BOOL needsToSyncSubviewsWithImages : 1;
}
Anyway, first I'll implement the public API, which is just the images property:
#pragma mark - Public API
#synthesize images = _images;
- (void)setImages:(NSArray *)images {
_images = [images copy];
needsToSyncSubviewsWithImages = YES;
}
Note that when you set the images array, I don't immediately create the subviews. For now, I just set the needsToSyncSubviewsWithImages flag so I'll know to do it during the layout phase.
#pragma mark - UIView overrides
Next, I need to override layoutSubviews so I can do the real work during the layout phase. The system sends me layoutSubviews during the layout phase if my subviews array has changed, or if my bounds has changed.
Because I'm a scroll view, and because a scroll view's contentOffset is really just an alias for its bounds.origin, the system sends me layoutSubviews a lot: every time the scroll view scrolls. So I want to be careful to do only necessary work in layoutSubviews.
- (void)layoutSubviews {
The first thing I do is call super, which takes lets auto layout work (if you're using it) and updates my scroll indicators (if they're visible).
[super layoutSubviews];
Next, if I got new images, I set up the subviews that display them.
if (needsToSyncSubviewsWithImages) {
[self syncSubviewsWithImages];
}
Next, if I've set up new subviews, or if I've changed size, I lay out my subviews' frames for the new size, and align to a page boundary.
if (needsToSyncSubviewsWithImages || !CGSizeEqualToSize(self.bounds.size, priorSize)) {
[self layoutForNewSize];
}
Finally, I update my state.
needsToSyncSubviewsWithImages = NO;
priorSize = self.bounds.size;
[self updatePageNumber];
}
Of course, I delegated all the real work to helper methods, so now I need to implement those.
#pragma mark - Implementation details
To synchronize my subviews with my images, I need to do three things. I need to make sure I've actually allocated my imageSubviews array, I need to make sure every image is in a subview, and I need to make sure I don't have any extra image subviews (in case my images array was made smaller).
- (void)syncSubviewsWithImages {
[self ensureImageSubviewsArrayExists];
[self putImagesInSubviews];
[self removeExtraSubviews];
}
- (void)ensureImageSubviewsArrayExists {
if (imageSubviews == nil) {
imageSubviews = [NSMutableArray arrayWithCapacity:self.images.count];
}
}
- (void)putImagesInSubviews {
[self.images enumerateObjectsUsingBlock:^(id obj, NSUInteger i, BOOL *stop) {
[self putImage:obj inSubviewAtIndex:i];
}];
}
- (void)removeExtraSubviews {
while (imageSubviews.count > self.images.count) {
[imageSubviews.lastObject removeFromSuperview];
[imageSubviews removeLastObject];
}
}
- (void)putImage:(UIImage *)image inSubviewAtIndex:(NSUInteger)i {
UIImageView *imageView = [self imageViewAtIndex:i];
imageView.image = image;
}
When I want to get the image view for an index, I might find that I haven't actually created enough subviews yet, so I create them on demand:
- (UIImageView *)imageViewAtIndex:(NSUInteger)i {
while (i >= imageSubviews.count) {
UIView *view = [[UIImageView alloc] init];
view.contentMode = UIViewContentModeScaleAspectFit;
view.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
[self addSubview:view];
[imageSubviews addObject:view];
}
return imageSubviews[i];
}
Note that I've set the autoresizingMask such that autoresizing won't actually modify my subview frames. Instead, I'll lay them out “manually”.
OK, now I need to implement the methods that set my subviews' frames and align to a page boundary when my size changes.
- (void)layoutForNewSize {
[self setSubviewFramesAndContentSize];
[self alignToNearestPage];
}
Setting the subview frames requires looping over them, laying them out from left to right. After I've laid out the last one, I know my contentSize. Note that I need to loop over imageSubviews only, not self.subviews, because self.subviews also contains the scroll indicators.
- (void)setSubviewFramesAndContentSize {
CGRect frame = self.bounds;
frame.origin = CGPointZero;
for (UIView *subview in imageSubviews) {
subview.frame = frame;
frame.origin.x += frame.size.width;
}
self.contentSize = CGSizeMake(frame.origin.x, frame.size.height);
}
To align to the nearest page, I set my contentOffset based on the last known page number and my new size.
- (void)alignToNearestPage {
self.contentOffset = CGPointMake(pageNumber * self.bounds.size.width, 0);
}
Finally, I need to update my page number every time I scroll, so I'll have it in case of rotation:
- (void)updatePageNumber {
// Note that self.contentOffset == self.bounds.origin.
CGRect bounds = self.bounds;
pageNumber = fmin(round(bounds.origin.x / bounds.size.width), self.images.count - 1);
}
#end
Now you can update ViewController to use the ImageScrollView. This mostly involves ripping stuff out:
-(void)configureImageTab{
//Page control
pageControl = [UIPageControl new];
pageControl.currentPageIndicatorTintColor = [UIColor blackColor];
pageControl.pageIndicatorTintColor = [UIColor grayColor];
[pageControl addTarget:self action:#selector(changePage) forControlEvents:UIControlEventValueChanged];
pageControl.translatesAutoresizingMaskIntoConstraints = NO;
//Configuring scrollView
self.scrollView = [[ImageScrollView alloc] initWithFrame:CGRectMake(0, 0, self.imageSegmentView.frame.size.width, self.imageSegmentView.frame.size.height-pageControl.frame.size.height)];
self.scrollView.backgroundColor = [UIColor clearColor];
self.scrollView.pagingEnabled = YES;
self.scrollView.delegate = self;
self.scrollView.userInteractionEnabled = YES;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
//Adding imageURLS to array
photos = #[ [UIImage imageNamed:#"createBootableUSBInstallDrive1"], [UIImage imageNamed:#"createBootableUSBInstallDrive2"], [UIImage imageNamed:#"createBootableUSBInstallDrive3"]];
self.scrollView.images = photos;
pageControl.numberOfPages = [photos count];
[self.imageSegmentView addSubview:self.scrollView];
[self.imageSegmentView addSubview:pageControl];
NSDictionary *views = #{#"pageControl" : pageControl, #"scrollView" : self.scrollView};
[self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[pageControl]-0-|" options:0 metrics:nil views:views]];
[self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[scrollView]|" options:0 metrics:nil views:views]];
[self.imageSegmentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]-1-[pageControl]-1-|" options:0 metrics:nil views:views]];
[pageControl addConstraint:[NSLayoutConstraint constraintWithItem:pageControl attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.imageSegmentView attribute:NSLayoutAttributeHeight multiplier:0 constant:30]];
}
You also need to change the declared type of scrollView to ImageScrollView in the header file. You can eliminate the viewWillLayoutSubviews, willRotateToInterfaceOrientation:duration:, and willAnimateRotationToInterfaceOrientation:duration: methods entirely.
I've uploaded my modified version of your test project to this github repository.
My UIScrollView is not setting its contentOffset when using zoomToRect.
I have an UIScrollView with an UIImageView inside. Scrolling and zooming itself is working so far. Now I want to give the scrollview at app start a certain zoomed rect of the image view. For this I implemented zoomToRect: and it is setting the zoomsScale correctly, but it does not set the contentOffset.
Expected outcome when using zoomToRect is that the UIScrollView zooms in or out according to the selected rect and set its contentOffset according to the origin coordinates of the rect given to the zoomToRect method.
The actual behaviour is that it zooms to the correct zoomScale but my UIImageView is always at the origin 0,0 and not the expected origin of the x (475) and y (520) coordinated of the rect I specified in zoomToRect.
My images size is 1473x1473.
Here is some the code
- (void)viewDidLoad {
CGRect bounds = self.view.frame;
_imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"bgImage.png"]];
self.containerView = [[UIView alloc] initWithFrame:bounds];
_containerView.contentMode = UIViewContentModeCenter;
_scrollView = [[UIScrollView alloc] initWithFrame:bounds];
_scrollView.delegate = self;
_scrollView.contentSize = _imageView.bounds.size;
_scrollView.minimumZoomScale = 0.2;
[_scrollView addSubview:_containerView];
[_containerView addSubview:_imageView];
[self.view addSubview:_scrollView];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[_scrollView zoomToRect:CGRectMake(475.0, 150.0, 520.0, 747.0) animated:NO];
}
#pragma mark UIScrollViewDelegate methods
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return _containerView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self printVisibleRectToConsole:scrollView];
CGSize newImageViewSizeWithScale = CGSizeMake(_imageView.bounds.size.width * _scrollView.zoomScale,
_imageView.bounds.size.height * _scrollView.zoomScale);
_scrollView.contentSize = newImageViewSizeWithScale;
}
My questions:
Why does zoomToRect does not set the contentOffset?
How can I get zoomToRect to change my contentOffset as expected?
The problem is that the view you're zooming (containerView) is not as big as the image view it contains (and which you actually want to zoom). Its frame is set to the frame of the view controller. You don't see this because a UIView doesn't clip its subviews by default.
You should initialize containerView with the bounds of your image view instead.
self.containerView = [[UIView alloc] initWithFrame:_imageView.bounds];