I have a UIScrollView and there are many views inside my scroll view. I am using autolayout and all my views are layed out in the same manner: Left and top spacing to the superview, width and height set. Everything scrolls just fine, however my page control stays whereever it is. It does not scroll with the other elements inside the scroll view. YES, I did check that the page control is inside the scroll view just like the other elements, and yes, I've quadruple-checked the constraints of the page control. It just won't scroll. What could be the problem? There are labels, another scroll view, text views, images views and they all scroll perfectly, it's just the page view that is problematic. Is there a bug with Xcode/iOS SDK, or am I missing something?
UPDATE: All the views inside my scroll view are inside a container view. Both the scroll view's and the container view's translatesAutoresizingMaskIntoConstraints property is set to NO. It's only the page control that doesn't obey it's contraints. Here is a screenshot from the Interface Builder:
I got it to work by:
1) Putting all views/controls inside a containerView and declaring it as a property.
2) Adding this code:
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
self.scrollView.contentSize = self.containerView.bounds.size;
}
- (void)viewDidLoad{
[super viewDidLoad];
//added with containerview from g8productions
self.containerView = [[UIView alloc] initWithFrame:CGRectMake(36, 59, 900, 1200)];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:self.containerView];
[self.scrollView setContentSize:self.containerView.bounds.size];
}
Hope either of these solutions work for you!
I made something like that and I update the page control in the scrollview delegate methods:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
// If tag == 0 means that is the scrollview for gallery
if (scrollView.tag == 0)
{
self.pageControllGallery.currentPage = self.sv1ScrollViewGallery.contentOffset.x/320;
}
else
{
// Change the bottom label text
int page = (scrollView.contentOffset.x / 320);
[UIView animateWithDuration:0.3 animations:^{
self.labelBottom.alpha = 0.0f;
self.labelBottom.text = [self.arrayOfScrollviews objectAtIndex:page];
self.labelBottom.alpha = 1.0f;
}];
}
}
I calculate the page and set that page in PageControll.
Don't forget to set the Scrollview delegate in .h and in the element.
To size the scroll view’s frame with Auto Layout, constraints must either be explicit regarding the width and height of the scroll view, or the edges of the scroll view must be tied to views outside of its subtree.
https://developer.apple.com/library/ios/releasenotes/General/RN-iOSSDK-6_0/index.html
If you can dump Autolayout it would be best.
Check out that link, they've got a Pure AutoLayout example at the bottom.
Basically use this code pattern:
#interface ViewController () {
UIScrollView *scrollView;
UIImageView *imageView;
}
#end
#implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
imageView.translatesAutoresizingMaskIntoConstraints = NO;
[scrollView addSubview:imageView];
[self.view addSubview:scrollView];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(scrollView,imageView);
[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]];
}
Related
I have 2 view controllers embedded in a view vertically. View controller A (uploader), and B (docList).
B contains a UICollectionView
All, methods except cellForItemAtIndexPath inside the datasource of the collection view get called correctly, and i double checked everything. There is 1 section. There are more than 0 rows. The size of the rows I return is smaller than the collection view, etc
Here's a diagram to illustrate the setup:
My issue is:
Unless i turn on setTranslatesAutoresizingMaskIntoConstraints to YES, cellForItemAtIndexPath will never be called. If i set that property to YES on the View Controller B's view, then it does get called. But the layout is then screwed up, because I am not using springs and struts. We only use constraints here.
Do you know what i can be doing wrong when embedding the view controller that contains the UICollectionView?
Here is the code that embeds the two view controllers' views, and sets them as child controllers:
- (MFFormBaseCell *)cellForComponent
{
self.cell = [[MFFormBaseCell alloc] initWithFrame:CGRectZero];
[self.cell addSubview: uploader.view];
[self.cell addSubview: docList.view];
UIView* uploaderView = uploader.view;
UIView* docListView = docList.view;
NSMutableArray* tempConstraints = [[NSMutableArray alloc]init];
[tempConstraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat: #"V:|-8-[uploaderView]-1-[docListView]-8-|"
options: NSLayoutFormatDirectionLeadingToTrailing metrics:nil
views: NSDictionaryOfVariableBindings(uploaderView, docListView)]];
[tempConstraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat: #"H:|-[uploaderView]-|"
options: NSLayoutFormatDirectionLeadingToTrailing metrics:nil
views: NSDictionaryOfVariableBindings(uploaderView)]];
[tempConstraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat: #"H:|-[docListView]|"
options: NSLayoutFormatDirectionLeadingToTrailing metrics:nil
views: NSDictionaryOfVariableBindings(docListView)]];
uploaderConstraints = [tempConstraints copy];
[self.cell addConstraints: uploaderConstraints];
[self.embedder addChildViewController:uploader];
[uploader didMoveToParentViewController:self.embedder];
[self.embedder addChildViewController:docList];
[docList didMoveToParentViewController:self.embedder];
docList.view.frame = self.cell.bounds;
return self.cell;
}
And here is the code from View Controller B, that sets up the UICollectionView and a vertical flow layout for it.
- (void)modelDidLoad
{
_dataSource = [[MFCardDataSource alloc] initWithData: self.cardModel];;
UICollectionViewFlowLayout *aFlowLayout = [[UICollectionViewFlowLayout alloc] init];
[aFlowLayout setScrollDirection:UICollectionViewScrollDirectionVertical];
_collectionView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:aFlowLayout];
[_collectionView setDelegate: self];
[_collectionView setDataSource:_dataSource];
[_collectionView setBackgroundColor:[UIColor clearColor]];
for (NSString* type in [MFCardCollectionModel typeArray])
[_collectionView registerClass:[MFImageCard class] forCellWithReuseIdentifier: type];
[_collectionView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:_collectionView];
[self registerConstraintsForView:_collectionView];
if ([self respondsToSelector:#selector(edgesForExtendedLayout)])
self.edgesForExtendedLayout = UIRectEdgeNone;
[super modelDidLoad];
}
And the contents of registerConstraintsForView:
-(void) registerConstraintsForView:(UIView*)collectionView
{
NSDictionary* metrics = #{ #"padding": #PADDING };
NSDictionary* views = NSDictionaryOfVariableBindings(_collectionView);
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|-padding-[_collectionView]-padding-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:metrics
views:views]];
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|-padding-[_collectionView]-padding-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:metrics
views:views]];
}
I got around the problem by Subclassing UICollectionView, and using the subclass instead.
On the subclass I overrode 2 methods:
- (void) setContentSize:(CGSize)contentSize
{
CGSize origSize = self.contentSize;
[super setContentSize:contentSize];
if (!CGSizeEqualToSize(contentSize, origSize))
{
[self invalidateIntrinsicContentSize];
}
}
- (CGSize) intrinsicContentSize
{
return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}
Then, i have a method inside my view controller which contains the Collection View, which calculates the required Height needed for the collection view.
I then call setContentSize with the calculated height on my subclass of Collection View, and that makes sure that it returns an intrinsicContentSize as tall as it needs to be to show all the card records inside it.
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.
I have a UICollectionView inside of a UITableViewCell, and the CollectionView cells show images that a user stores in the app. When the user taps the cell, the image is presented full screen. So I created a UIViewController, added a UIScrollView to it for zooming, then added a UIImageView to the ScrollView. Everything works nicely until the user changes the orientation of the phone. When they change to landscape the view adjusts but the scrollview and image view do not adjust, and therefore just show on the left side of the screen; they seem to keep the same bounds. I tried to fix this by following apple's tech note using the pure layout approach and adding constraints. But when I do this, the image that is displayed is zoomed all the way in and I cannot get it to fit correctly. When I use an NSLog statement to determine the frames, they are all set to self.view.frame and therefore printout the size of the screen, but the image still shows at full size. Everything is created programmatically, no xib files.
I would love to avoid the constraints, but they seem necessary for the user to be able to see images correctly in landscape mode. I have tried setting the contentSize of the scrollview, setting clipsToBounds to YES and changing the contentMode but none of these things have any effect. Any help is greatly appreciated!
collectionView:didSelectItemAtIndexPath:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
NSDictionary *viewsDictionary;
//creating ViewController to be presented
UIViewController *fullImageView = [[UIViewController alloc] init];
fullImageView.view.userInteractionEnabled = YES;
fullImageView.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[[UIApplication sharedApplication] setStatusBarHidden:YES];
_selectedImageView = [[UIImageView alloc] initWithFrame:self.view.frame];
NSString *key = [self.viewItem.imageKeyArray objectAtIndex:indexPath.row];
_selectedImageView.image = [self.viewItem.itemImages objectForKey:key];
_selectedImageView.clipsToBounds = YES;
_selectedImageView.contentMode = UIViewContentModeScaleAspectFit;
_selectedImageView.translatesAutoresizingMaskIntoConstraints = NO;
UIScrollView *imageScrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
imageScrollView.delegate = self;
imageScrollView.minimumZoomScale = 1;
imageScrollView.maximumZoomScale = 2;
imageScrollView.translatesAutoresizingMaskIntoConstraints = NO;
[fullImageView.view addSubview:imageScrollView];
[imageScrollView addSubview:_selectedImageView];
viewsDictionary = NSDictionaryOfVariableBindings(imageScrollView, _selectedImageView);
[fullImageView.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageScrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[fullImageView.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[imageScrollView]|" options:0 metrics: 0 views:viewsDictionary]];
[imageScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_selectedImageView]|" options:0 metrics: 0 views:viewsDictionary]];
[imageScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_selectedImageView]|" options:0 metrics: 0 views:viewsDictionary]];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(dismissFullImageView)];
[fullImageView.view addGestureRecognizer:tap];
[self presentViewController:fullImageView animated:YES completion:nil];
}
I am trying to create a header view for my uiTableView (not a section header, I already have those.) I have set up an XIB in interface builder. All the connections are hooked up and it runs beautifully... except the table doesn't give it enough room! My problem is that the top of the table overlaps the table's header by a little.
My XIB is setup with autlayout for all the buttons, and IB is happy that the constraints don't conflict / ambiguous. The view is set to Freeform size, which in my case ended up being 320 x 471. Then in constraints for the view, I set an intrinsic size for the view of the same.
Now this works perfectly with my table. Everything looks great. But if I manually change any of the fonts in the header view with code, the layout makes the view bigger, and it ends up underneath my table.
Any ideas how to get the tableviewcontroller to leave enough room for the header view after setting fonts and sizes? I hope I've made sense explaining this.
Note: A Swift 3+ version can be found here: https://gist.github.com/marcoarment/1105553afba6b4900c10#gistcomment-1933639
The idea is to calculate header's height with help of systemLayoutSizeFittingSize:targetSize.
Returns the size of the view that satisfies the constraints it holds.
Determines the best size of the view considering all constraints it
holds and those of its subviews.
After changing header's height it is necessary to reassign tableHeaderView property to adjust table cells.
Based on this answer: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
- (void)sizeHeaderToFit
{
UIView *header = self.tableView.tableHeaderView;
[header setNeedsLayout];
[header layoutIfNeeded];
CGFloat height = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect frame = header.frame;
frame.size.height = height;
header.frame = frame;
self.tableView.tableHeaderView = header;
}
I encountered a similar problem when I was trying to set my table view header to a custom view I defined with auto layout using interface builder.
When I did so, I would find that it would be obscured by a section header.
My work around involved using a "dummy view" as the table header view and then adding my custom view to that dummy view as a subview. I think this allows auto layout to configure the appearance as per the constraints and the containing view's frame. This is probably not as elegant as vokilam's solution, but it works for me.
CGRect headerFrame = CGRectMake(0, 0, yourWidth, yourHeight);
UIView *tempView = [[UIView alloc] initWithFrame:headerFrame];
[tempView setBackgroundColor:[UIColor clearColor]];
YourCustomView *customView = [[YourCustomView alloc] initWithFrame: headerFrame];
[tempView addSubview:movieHeader];
self.tableView.tableHeaderView = tempView;
Because your tableHeaderView is inited form xib with auto layout, so the constraints of your custom headerView and the superView is unknown.You should add the constraints of your custom headerView:
1.in viewDidLLoad assign your custom headerView to the tableView's tableHeaderView
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *yourHeaderView = [[[NSBundle mainBundle] loadNibNamed:#"yourHeaderView" owner:nil options:nil] objectAtIndex:0];
//important:turn off the translatesAutoresizingMaskIntoConstraints;apple documents for details
yourHeaderView.translatesAutoresizingMaskIntoConstraints = NO;
self.tableView.tableHeaderView = yourHeaderView;
}
2.in - (void)updateViewConstraints,add the essential constraints of your custom headerView
- (void)updateViewConstraints
{
NSDictionary *viewsDictionary = #{#"headerView":yourHeaderView};
NSArray *constraint_V = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[headerView(121)]"
options:0
metrics:nil
views:viewsDictionary];
NSArray *constraint_H = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[headerView(320)]"
options:0
metrics:nil
views:viewsDictionary];
[self.headerView addConstraints:constraint_H];
[self.headerView addConstraints:constraint_V];
NSArray *constraint_POS_V = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[headerView]"
options:0
metrics:nil
views:viewsDictionary];
NSArray *constraint_POS_H = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[headerView]"
options:0
metrics:nil
views:viewsDictionary];
[self.tableView addConstraints:constraint_POS_V];
[self.tableView addConstraints:constraint_POS_H];
[super updateViewConstraints];
}
OK!
PS:Here is the related document:Resizing the View Controller’s Views
What solved my issue was this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sizeHeaderToFit()
}
private func sizeHeaderToFit() {
if let headerView = tableView.tableHeaderView {
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
var newFrame = headerView.frame
// Needed or we will stay in viewDidLayoutSubviews() forever
if height != newFrame.size.height {
newFrame.size.height = height
headerView.frame = newFrame
tableView.tableHeaderView = headerView
}
}
}
I found this solution somewhere and worked like charm, I can't remember where though.
I found vokilam's answer to work great. Here's his solution in Swift.
func sizeHeaderToFit() {
guard let header = tableView.tableHeaderView else { return }
header.setNeedsLayout()
header.layoutIfNeeded()
let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
header.frame.size.height = height
tableView.tableHeaderView = header
}
The other answers pointed me in the right direction for getting the tableHeaderView to resize appropriately. However, I was getting a gap between the header and the tableView cells. This could also cause your cells to overlap with your header, depending on if the header grew or got smaller.
I went from this:
tableView.reloadData()
let height = tableViewHeader.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
tableView.tableHeaderView?.frame.size.height = height
To fix the gap/overlap all I had to do was move tableView.reloadData() to after I set the new height instead of before.
To this:
let height = tableViewHeader.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
tableView.tableHeaderView?.frame.size.height = height
tableView.reloadData()
In my case systemLayoutSizeFittingSize return incorrect size, because one of the subviews use aspect ratio (width to height) 4:1 constraint.
After I change it to constant height constraint value (100), it begin to works as expected.
Combining answers by #SwiftArchitect, #vokilam, and #mayqiyue, and using a programmatically created tableHeaderView instead of a nib (although, I don't see any reason why it shouldn't work with a nib), and under iOS 10.x/Xcode 8.2.1, here is what I have working:
Create the view in your view controller's -viewDidLoad or -init methods, or wherever you're using the table view:
MyCustomTableHeaderView *myCustomTableHeaderView = [[MyCustomTableHeaderView alloc] init]; // Don't set its frame
myCustomTableHeaderView.translatesAutoresizingMaskIntoConstraints = NO;
self.myTableView.tableHeaderView = myCustomTableHeaderView;
self.myCustomTableHeaderView = myCustomTableHeaderView; // We set and update our constraints in separate methods so we store the table header view in a UIView property to access it later
Wherever you set up your custom header view's constraints (could be in `updateConstraints but this method could be called multiple times):
// We use Pure Layout in our project, but the workflow is the same:
// Set width and height constraints on your custom view then pin it to the top and left of the table view
// Could also use VFL or NSLayoutConstraint methods
[self.myCustomTableHeaderView autoSetDimension:ALDimensionWidth toSize:CGRectGetWidth(self.superview.frame)]; // Width set to tableview's width
[self.myCustomTableHeaderView autoSetDimension:ALDimensionHeight toSize:height]; // Whatever you want your height to be
[self.myCustomTableHeaderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.myCustomTableHeaderView autoPinEdgeToSuperviewEdge:ALEdgeLeft];
// This is important
[self.myCustomTableHeaderView layoutIfNeeded]; // We didn't need to call -setNeedsLayout or reassign our custom header view to the table view's tableHeaderView property again, as was noted in other answers
sizeHeaderToFit suggested by #vokilam didn't work for me.
What worked is a modified version of #mayqiyue, invoked during viewDidLoad, wired height, equal width to table view.
// Anchor the headerview, and set width equal to superview
NSDictionary *viewsDictionary = #{#"headerView":self.tableView.tableHeaderView,
#"tableView":self.tableView};
[self.tableView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[headerView]"
options:0
metrics:nil
views:viewsDictionary]];
[self.tableView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[headerView]"
options:0
metrics:nil
views:viewsDictionary]];
[self.tableView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[headerView(==tableView)]"
options:0
metrics:nil
views:viewsDictionary]];
[self.tableView.tableHeaderView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[headerView(121)]"
options:0
metrics:nil
views:viewsDictionary]];
Below code has worked for me :-
Add this in your UIView class -
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
self.frame = CGRectMake(0, 0, self.frame.size.width, [[self class] height]);
}
return self;
}
#rmigneco answer worked for me, so here's the swift version of it:
let headerFrame = CGRectMake(0, 0, 100, 1);
let tempView = UIView(frame: headerFrame);
tempView.backgroundColor = UIColor.clearColor()
let yourCustomView = CustomView(frame: headerFrame)
tempView.addSubview(yourCustomView)
self.tableView.tableHeaderView = tempView