I want to modify the section header view when user scrolls down, something similar to this in Music app
(Notics how the view background color has changed and got a bottom border)
Is there a good way to track when the view is on top of the section or in scrolling position?
Update:
My only solution so far is to keep an array of all the section header views and change the view of the first visible section in scrollViewDidScroll: delegate method (getting the first visible section index using tableView.indexPathsForVisibleRows array)
If anyone can come up with a simpler way, that would be great!
You can modify the color (and whatever else you want) of the section header view in the scrollViewDidScroll method. This example darkens the color of the floating header view as the user scrolls down, and keeps that color's white value between 0.9 and 0.6. It also unhides a bottom border line in the header view if you scroll down by more than 5 points.
The .m file for RDHeaderView:
- (id)init{
self = [super init];
if (self) {
UIView *line = [[UIView alloc] init];
[line setTranslatesAutoresizingMaskIntoConstraints:NO];
line.backgroundColor = [UIColor darkGrayColor];
[self addSubview:line];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[line]|" options:0 metrics:nil views:#{#"line":line}]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[line]|" options:0 metrics:nil views:#{#"line":line}]];
[line addConstraint:[NSLayoutConstraint constraintWithItem:line attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:1]];
self.bottomLine = line;
self.bottomLine.hidden = YES;
}
return self;
}
The relevant methods in the table view controller:
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
RDHeaderView *header = [[RDHeaderView alloc] init];
header.contentView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
return header;
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSInteger topSection = [[self.tableView indexPathsForVisibleRows].firstObject section];
NSInteger sectionYOffset = [self.tableView rectForHeaderInSection:topSection].origin.y;
RDHeaderView *pinnedHeader = (RDHeaderView *)[self.tableView headerViewForSection:topSection];
pinnedHeader.bottomLine.hidden = ((scrollView.contentOffset.y - sectionYOffset) > 5)? NO: YES;
CGFloat colorOffset = fmaxf(0.6, 0.9 - (scrollView.contentOffset.y - sectionYOffset)/1000.0);
if (colorOffset > 0.9) colorOffset = 0.9;
pinnedHeader.contentView.backgroundColor = [UIColor colorWithWhite:colorOffset alpha:1];
}
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 80;
}
Maybe a simpler solution exists, but this woud be one way of achieving what you want:
remove the tableHeader, and add it as a separate subview to your viewControllers view (N.B. do not use a uitableviewController, as that viewController has the tableview as its view, and you don't want that)
move the tableView down so that it fits right under that former-tableheader view
compute and set the contentOffset of the tableView (it is a UIScrollView) so that the cells don't seem to jump to a new position.
Also, you could try the return a sectionHeaderView (which scrolls along with the tableView down, but not up, see for example in you contacts list how this works). Play around with that view being a cell or a section header.
Related
Here's a breakdown of my UIView:
UIView at the top. I'm using this as a container view that has other objects in it.
UITableView below the container view.
I want the scrolling to start with the UITableView going over the container view and then scroll through the contents of the table.
I want my table to have a header view that will stick to the top under my UINavigationController.
If I do something like myTableView.tableHeaderView, it scrolls under the navigation bar.
If I use the table view methods to add a header view, it's frozen in the middle of the screen (where it starts).
Here's my viewDidLayoutSubviews code:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self.myTableView setContentInset:UIEdgeInsetsMake(self.containerView.bounds.size.height, 0.f, 0.f, 0.f)];
[self.myTableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:NO];
float headerImageYOffset = 88 + self.containerView.bounds.size.height - self.view.bounds.size.height;
CGRect headerImageFrame = self.containerView.frame;
headerImageFrame.origin.y = headerImageYOffset;
}
Here's my header code and scroll code:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
// This is made in Interface Builder
return self.headerView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 50;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat scrollOffset = -scrollView.contentOffset.y;
CGFloat yPos = scrollOffset - _containerView.bounds.size.height;
_containerView.frame = CGRectMake(0, yPos, _containerView.frame.size.width, _containerView.frame.size.height);
}
What needs to be improved to allow the header to scroll with the table and stay under the navigation bar at the top?
If other code needs to be seen please let me know.
EDIT:
I added more code that goes with what I'm trying to accomplish. See above.
You don't need to implement scrollViewDidScroll:, just add an auto layout constraint between the parent of the table view and the header:
- (void)viewDidLoad {
NSLayoutConstraint* contraint = [NSLayoutConstraint constraintWithItem:self.headerView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:self.topLayoutGuide.length];
[self.view addConstraint:contraint];
}
I've been trying for quite awhile to build a parallax-style table view header that's comprised of an image, similar to the Yahoo News Digest App, or when viewing a business in Maps.app. (When you rubber-band the table the image height grows, and when scrolling down the image appears to scroll slightly slower).
Here's an demonstrative video courtesy of APParallaxHeader:
https://www.youtube.com/watch?v=7-JMdapWXGU
The best tutorial I was able to find was this tutorial, which basically consists of adding the image view as a subview of the table view. While that mostly works, adding as a subview to UITableView is pretty undocumented, and in my testing does not appear to work with Auto Layout and thus rotation doesn't play nicely.
The library I linked above, APParallaxHeader, seems to work, but it's implementation is really confusing, and seems to be swizzling if I'm not wrong?
Is there a simple way to do this that I'm just completely overlooking?
After giving this problem some more thought, I think the best way to duplicate that look is with a scrollview containing an image view that's behind (in the z-order sense) and extending below (in the y-direction sense) the top of a table view. In the test I did, I gave the table view a header (in IB) that was 100 points tall, and with a clear background color (the table also needs a clear background color). The scroll view and the table view were both pinned to the sides of the controller's main view, and to the top layout guide (the controller is embedded in a navigation controller, that was set to have its view not go under the top bar). The table view was also pinned to the bottom of the view, and the scroll view was given a fixed height of 200. I gave the scroll view an initial offset of 50 points, so that when you start to pull down on the table, the scroll view can scroll more content into view from the top, while also revealing more content at the bottom (the scroll view's offset is moving at 1/2 the rate of the table view's offset). Once the table view's offset reaches -50, I stop changing the scroll view's offset, and start zooming.
#define ZOOMPOINT 50
#interface ViewController () <UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate>
#property (weak, nonatomic) IBOutlet UIScrollView *sv;
#property(weak,nonatomic) IBOutlet UITableView *tableView;
#property (strong,nonatomic) UIImageView *iv;
#end
#implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
self.sv.minimumZoomScale = 1.0;
self.sv.maximumZoomScale = 2.0;
self.sv.delegate = self;
self.iv = [UIImageView new];
self.iv.contentMode = UIViewContentModeScaleAspectFill;
self.iv.translatesAutoresizingMaskIntoConstraints = NO;
}
-(void)viewDidLayoutSubviews {
[self.iv removeFromSuperview];
[self.sv addSubview:self.iv];
[self.sv addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[iv(==width)]|" options:0 metrics:#{#"width":#(self.tableView.frame.size.width)} views:#{#"iv":self.iv}]];
[self.sv addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[iv(==250)]|" options:0 metrics:nil views:#{#"iv":self.iv}]];
self.iv.image = [UIImage imageNamed:#"img.jpg"]; // the image I was using was 500 x 500
self.sv.contentOffset = CGPointMake(0, ZOOMPOINT);
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
if ([scrollView isEqual:self.sv]) {
return self.iv;
}else{
return nil;
}
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView != self.sv) {
if (scrollView.contentOffset.y < -ZOOMPOINT) {
[self.sv setZoomScale:(scrollView.contentOffset.y + ZOOMPOINT)/-100 + 1]; // the -100 is arbitrary, change to affect the sensitivity of the zooming
}else{
self.sv.contentOffset = CGPointMake(0, ZOOMPOINT + scrollView.contentOffset.y/2.0);
}
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 20;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
cell.textLabel.text = [NSString stringWithFormat:#"Cell %ld", (long)indexPath.row];
return cell;
}
I've uploaded a copy of this project here, http://jmp.sh/LRKF0nM
I thought I'd throw out another idea that doesn't use a separate scroll view. I think this works a little better with the way it expands. So, in this attempt, I just add the image view as a subview of the main view, and placed it so 1/2 as much of the image view is above the top of the header (out of view) as below the header (initially hidden by the table rows). When pulling down the table, the view is moved down at half the rate of the pull down (by adjusting a constraint), so the top and the bottom of the image come into view together, then from there, I do the expansion by using a transform.
#import "ViewController.h"
#define ZOOMPOINT -60
#interface ViewController () <UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate>
#property(weak,nonatomic) IBOutlet UITableView *tableView;
#property (weak, nonatomic) IBOutlet UIView *tableHeader;
#property (strong,nonatomic) UIImageView *iv;
#property (strong,nonatomic) NSLayoutConstraint *topCon;
#end
#implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
self.iv = [UIImageView new];
self.iv.contentMode = UIViewContentModeScaleToFill; //UIViewContentModeScaleAspectFill;
self.iv.translatesAutoresizingMaskIntoConstraints = NO;
self.edgesForExtendedLayout = UIRectEdgeNone;
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.view addSubview:self.iv];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[iv]|" options:0 metrics:nil views:#{#"iv":self.iv}]];
self.topCon = [NSLayoutConstraint constraintWithItem:self.iv attribute:NSLayoutAttributeTop relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:ZOOMPOINT/2.0];
[self.iv addConstraint:[NSLayoutConstraint constraintWithItem:self.iv attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:self.tableHeader.frame.size.height - ZOOMPOINT*1.5]];
[self.view addConstraint:self.topCon];
[self.view layoutIfNeeded];
self.iv.image = [UIImage imageNamed:#"img.jpg"];
[self.view sendSubviewToBack:self.iv];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.y < 0 && scrollView.contentOffset.y > ZOOMPOINT) {
self.topCon.constant = ZOOMPOINT/2.0 - scrollView.contentOffset.y/2.0;
}else if (scrollView.contentOffset.y <= ZOOMPOINT) {
self.iv.transform = CGAffineTransformMakeScale(1 - (scrollView.contentOffset.y - ZOOMPOINT)/200, 1 - (scrollView.contentOffset.y - ZOOMPOINT)/200);
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 20;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
cell.textLabel.text = [NSString stringWithFormat:#"Cell %ld", (long)indexPath.row];
return cell;
}
The project can be found here, http://jmp.sh/7PXzISZ
Hi heres my medium post on the subject:
https://medium.com/#jeremysh/creating-a-sticky-header-for-a-uitableview-40af71653b55#.hi79wgtsd
Just riffing here, but if the header's natural frame is frame, and you've got the table's scroll view delegate set, then the zoomed frame would be very similar to:
// in scrollViewDidScroll:
// when the table view is scrolled beyond the header, contentOffset.y is negative
CGFloat headerAspect = frame.size.width / frame.size.height;
CGFloat offsetY = tableView.contentOffset.y;
CGFloat offsetX = offsetY * headerAspect;
// this will enlarge frame since offsets are < 0
frame = CGInsetRect(frame, offsetY, offsetX);
// slide it down to keep the top at the top of the header
frame = CGRectOffset(frame, 0, offsetY / 2.0);
Couple this with setting the contentMode on the image view to UIViewContentModeScaleToFill, and that should be a decent start.
Okay, here's an answer that has the benefit of getting built and tried out.
I found it too hard to manipulate the frame of the table's actual header view, so I added a subview to the table above the rows. In order for that view to show up as a regular table header, I gave the table a fixed sized, transparently colored header view.
The main idea is like what I answered above: using the table's content offset as the parameter for modifying the image view frame, and the imageView's content mode (corrected to UIViewContentModeScaleAspectFill) to provide the zooming effect as the frame changes.
Here's the whole view controller. This is built from a storyboard where the view controller is inside a navigation controller. It has nothing more than a table view filling its view, with the datasource and delegate set.
#import "ViewController.h"
// how much of the image to show when the table is un-scrolled
#define HEADER_HEIGHT (100.0)
// the height of the image scaled down to fit in the header. the real image can/should be taller than this
// i tested this with a 600x400 image
#define SCALED_IMAGE_HEIGHT (200.0)
// zoom image up to this offset
#define MAX_ZOOM (150.0)
#interface ViewController () <UITableViewDataSource, UITableViewDelegate>
#property(weak,nonatomic) IBOutlet UITableView *tableView;
#end
#implementation ViewController
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// build the header in view will appear after other layout constraints are applied
UIImageView *headerView = (UIImageView *)[self.tableView viewWithTag:99];
if (!headerView) {
headerView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"landscape.png"]];
headerView.tag = 99;
headerView.contentMode = UIViewContentModeScaleAspectFill;
headerView.clipsToBounds = YES;
headerView.frame = CGRectMake(0, HEADER_HEIGHT, self.view.bounds.size.width, SCALED_IMAGE_HEIGHT);
[self.tableView addSubview:headerView];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offsetY = -self.tableView.contentOffset.y - 64;
// minus 64 is kind of a bummer here. this calc wants the offset to be 0
// when no scrolling has happened. for some reason my table view starts at -64
CGFloat clamped = MIN(MAX(offsetY, 0), MAX_ZOOM);
CGFloat origin = -HEADER_HEIGHT - clamped;
CGFloat height = SCALED_IMAGE_HEIGHT + clamped;
UIImageView *headerView = (UIImageView *)[self.tableView viewWithTag:99];
CGRect frame = headerView.frame;
frame.origin.y = origin;
frame.size.height = height;
headerView.frame = frame;
}
// this is a trick to make the view above the header visible: make the table header a clear UIView
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, HEADER_HEIGHT)];
view.backgroundColor = [UIColor clearColor];
return view;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return HEADER_HEIGHT;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 30;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
cell.textLabel.text = [NSString stringWithFormat:#"Cell %ld", indexPath.row];
return cell;
}
#end
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 custom UIView which I want to be able to reuse throughout my application as a section header in tableviews. Here is the applicable code for that (this is basically all it is doing):
-(id) initWithTitle:(NSString *)title{
self = [super init];
if(self){
_title = title;
self.translatesAutoresizingMaskIntoConstraints = false;
[self addSubview:self.titleLabel];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"H:|-%#-[_titleLabel]-%#-|", HORIZONTAL_SPACE, HORIZONTAL_SPACE] options:0 metrics:nil views:NSDictionaryOfVariableBindings(self, _titleLabel)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"V:|-%#-[_titleLabel]-%#-|", VERTICAL_SPACE, VERTICAL_SPACE] options:0 metrics:nil views:NSDictionaryOfVariableBindings(self, _titleLabel)]];
[self updateConstraintsIfNeeded];
}
return self;
}
I am using it like this:
- (UIView*)tableView:(UITableView*)tableView
viewForHeaderInSection:(NSInteger)section
{
NSString *headerText = _headers[section];
if(headerText.length){
AWSmallTextSectionHeader *header = [[AWSmallTextSectionHeader alloc] initWithTitle:headerText];
return header;
}
return [UIView new];
}
The problem is that when the views draw to the screen they all start at the same origin...This is what is looks like at the top of the tableview:
It is really weird though because it is acting as if it is in the correct section, for example, if you start scrolling and one of the sections go offscreen, the view that corresponds to that section header will disappear.
I have a feeling it has to do with the way I am adding my constraints but I am not sure.
Thanks
Edit (this is also implemented)
- (CGFloat)tableView:(UITableView*)tableView
heightForHeaderInSection:(NSInteger)section
{
if (section == SECTION_INDEX_PHONE) {
return 60.f; //CGRectGetHeight(self.phoneSectionHeader.frame);
} else if (section == SECTION_INDEX_FEEDBACK) {
return 40.f; //CGRectGetHeight(self.feedbackSectionHeader.frame);
}
return 5.0;
}
Should you at some point set the bounds for the header section view?
Also ensure you're adapting the heightForHeaderInSection:
This method only works correctly when tableView:heightForHeaderInSection: is also implemented.
self.translatesAutoresizingMaskIntoConstraints = false; was the problem.
Once I got rid of that it was working correctly.
I am working with a UITableViewController. I have a table of items that the user can delete if he goes into edit more. When he goes into edit mode, I want to show a header that gives an option to delete all items. At the same time, it should show a label giving information about how much space is being used. I want this to automatically resize if the device goes into landscape mode. From what I can tell, I need to use autolayout to do this.
I would have loved to set up the header in a UIView designed in the Storyboard, but the Storyboard only allows view controllers, not views. I know I could have a XIB file hold it, but I would rather avoid that if I could.
To start with, I've overridden the editing property so that I can redraw the table section when in editing mode.
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
NSIndexSet *set = [NSIndexSet indexSetWithIndex:0];
[self.tableView reloadSections:set withRowAnimation:UITableViewRowAnimationAutomatic];
}
I use this code to insert the section header when appropriate:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView];
else
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView].frame.size.height;
else
return 0;
}
The magic happens in the - headerView method. It returns a UIView *, getting it from a cache if necessary. It adds the button and the label and then puts in the constraints. I've used these same constraints in the Storyboard and I haven't had any problems.
- (UIView *)headerView
{
if (headerView)
return headerView;
float w = [[UIScreen mainScreen] bounds].size.width;
UIButton *deleteAllButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[deleteAllButton setTitle:#"Delete All" forState:UIControlStateNormal];
CGRect deleteAllButtonFrame = CGRectMake(8.0, 8.0, 30.0, 30); // The autolayout should resize this.
[deleteAllButton setFrame:deleteAllButtonFrame];
deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[deleteAllButton setContentHuggingPriority:252 forAxis:UILayoutConstraintAxisHorizontal];
[deleteAllButton setContentCompressionResistancePriority:751 forAxis:UILayoutConstraintAxisHorizontal];
CGRect textFrame = CGRectMake(47.0, 8.0, 30.0, 30); // The autolayout should resize this.
UILabel *currSizeText = [[UILabel alloc] initWithFrame:textFrame];
currSizeText.text = #"You have a lot of text here telling you that you have stuff to delete";
currSizeText.translatesAutoresizingMaskIntoConstraints = NO;
currSizeText.adjustsFontSizeToFitWidth = YES;
CGRect headerViewFrame = CGRectMake(0, 0, w, 48);
headerView = [[UIView alloc] initWithFrame:headerViewFrame];
//headerView.autoresizingMask = UIViewAutoresizingNone;//UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
//headerView.translatesAutoresizingMaskIntoConstraints = NO;
[headerView addSubview:deleteAllButton];
[headerView addSubview:currSizeText];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(deleteAllButton, currSizeText);
[headerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[deleteAllButton]-[currSizeText]-|"
options:0
metrics:nil
views:viewsDictionary]];
[headerView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
[headerView addConstraint:[NSLayoutConstraint constraintWithItem:currSizeText
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
return headerView;
}
Right now, everything is working beautifully. The button keeps a constant size (because the hugging and compression resistance are higher than the label's) and the label will change its text to fit the available space. It resizes when I rotate the device. The vertical centering seems off on the label, but I am willing to overlook that for now.
However, when I first setup the section header, I get an annoying autolayout warning.
2014-02-07 11:25:19.770 ErikApp[10704:70b] 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:0xb9a4ad0 H:|-(NSSpace(20))-[UIButton:0xb99e220] (Names: '|':UIView:0xb9a4680 )>",
"<NSLayoutConstraint:0xb9a4bf0 H:[UIButton:0xb99e220]-(NSSpace(8))-[UILabel:0xb99f530]>",
"<NSLayoutConstraint:0xb9a4c20 H:[UILabel:0xb99f530]-(NSSpace(20))-| (Names: '|':UIView:0xb9a4680 )>",
"<NSAutoresizingMaskLayoutConstraint:0xa2d1680 h=--& v=--& H:[UIView:0xb9a4680(0)]>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0xb9a4bf0 H:[UIButton:0xb99e220]-(NSSpace(8))-[UILabel:0xb99f530]>
Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
My first thought was to change the returned UIView property translatesAutoresizingMaskIntoConstraints to NO. When I do that, I get a crash instead of a warning. Not exactly an improvement.
2014-02-07 10:49:13.041 ErikApp[10597:70b] *** Assertion failure in -[UITableView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2903.23/UIView.m:8540
2014-02-07 10:49:13.383 ErikApp[10597:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super.'
Does anyone have a suggestion as to what to do to get rid of the warning?
It seems that when your section is reloading, the UITableView at some moment has a reference to both the old section header and the new one. And if it is the same view, some issues appear. So you must always provide a different view from the tableView:viewForHeaderInSection: method.
Sometimes it is really useful to have a single instance to be presented in a section header. For this purpose you need to create a new view each time you are asked for a section header and put your custom view inside it, configuring constraints appropriately. Here's an example:
#property (strong, nonatomic) UIView *headerContentView;
- (void)viewDidLoad {
// Create the view, which is to be presented inside the section header
self.headerContentView = [self loadHeaderContentView];
// Note that we have to set the following property to NO to prevent the unsatisfiable constraints
self.headerContentView.translatesAutoresizingMaskIntoConstraints = NO;
}
- (UIView *)loadHeaderContentView {
// Here you instantiate your custom view from a nib
// or create it programmatically. Speaking in terms
// of the OP, it should look like the following. (Note:
// I have removed all the frame-related code as your are
// not supposed to deal with frames directly with auto layout.
// I have also removed the line setting translatesAutoresizingMaskIntoConstraints property
// to NO of the headerContentView object as we do it explicitly in viewDidLoad.
UIButton *deleteAllButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[deleteAllButton setTitle:#"Delete All" forState:UIControlStateNormal];
deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[deleteAllButton setContentHuggingPriority:252 forAxis:UILayoutConstraintAxisHorizontal];
[deleteAllButton setContentCompressionResistancePriority:751 forAxis:UILayoutConstraintAxisHorizontal];
UILabel *currSizeText = [[UILabel alloc] init];
currSizeText.text = #"You have a lot of text here telling you that you have stuff to delete";
currSizeText.translatesAutoresizingMaskIntoConstraints = NO;
currSizeText.adjustsFontSizeToFitWidth = YES;
UIView *headerContentView = [[UIView alloc] init];
[headerContentView addSubview:deleteAllButton];
[headerContentView addSubview:currSizeText];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(deleteAllButton, currSizeText);
// In the original post you used to have an ambigious layout
// as the Y position of neither button nor label was set.
// Note passing NSLayoutFormatAlignAllCenterY as an option
[headerContentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[deleteAllButton]-[currSizeText]-|"
options:NSLayoutFormatAlignAllCenterY
metrics:nil
views:viewsDictionary]];
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0]];
// Here setting the heights of the subviews
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:deleteAllButton
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
[headerContentView addConstraint:[NSLayoutConstraint constraintWithItem:currSizeText
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:headerContentView
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0]];
return headerContentView;
}
- (UIView *)headerView {
UIView *headerView = [[UIView alloc] init];
[headerView addSubview:self.headerContentView];
NSDictionary *views = #{#"headerContentView" : self.headerContentView};
NSArray *hConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[headerContentView]|" options:0 metrics:nil views:views];
NSArray *vConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[headerContentView]|" options:0 metrics:nil views:views];
[headerView addConstraints:hConstraints];
[headerView addConstraints:vConstraints];
return headerView;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (self.isEditing)
return [self headerView];
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
// You need to return a concrete value here
// and not the current height of the header.
if (self.isEditing)
return 48;
return 0;
}
I created a GitHub repo for this post here:https://github.com/bilobatum/AnimatedTableHeaderDemo
This solution implements a table header view, i.e., self.tableView.tableHeaderView, instead of section headers for a table view with a single section.
The table header view and its subviews are colored for testing purposes. An arbitrary table header height is chosen for testing purposes.
The table header is lazily instantiated and animates into place when the table view enters editing mode. An animation hides the table header when the table view exits editing mode.
In general, you're not supposed to set frames when using Auto Layout. However, a table header is a special case in a sense. Don't use Auto Layout to size or position a table header. Instead, you must set a table header's frame (actually, you only need to set the rect's height). In turn, the system will translate the table header's frame into constraints.
However, it's okay to use Auto Layout on the table header's subviews. Some of these constraints are installed on the table header view.
#interface ViewController ()
#property (nonatomic, strong) NSArray *mockData;
#property (nonatomic, strong) UIButton *deleteAllButton;
#property (nonatomic, strong) UILabel *label;
#property (nonatomic, strong) UIView *headerView;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = #"Fruit";
self.mockData = #[#"Orange", #"Apple", #"Pear", #"Banana", #"Cantalope"];
self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (UIButton *)deleteAllButton
{
if (!_deleteAllButton) {
_deleteAllButton = [[UIButton alloc] init];
_deleteAllButton.backgroundColor = [UIColor grayColor];
[_deleteAllButton setTitle:#"Delete All" forState:UIControlStateNormal];
_deleteAllButton.translatesAutoresizingMaskIntoConstraints = NO;
[_deleteAllButton addTarget:self action:#selector(handleDeleteAll) forControlEvents:UIControlEventTouchUpInside];
}
return _deleteAllButton;
}
- (UILabel *)label
{
if (!_label) {
_label = [[UILabel alloc] init];
_label.backgroundColor = [UIColor yellowColor];
_label.text = #"Delete all button prompt";
_label.translatesAutoresizingMaskIntoConstraints = NO;
}
return _label;
}
- (UIView *)headerView
{
if (!_headerView) {
_headerView = [[UIView alloc] init];
// WARNING: do not set translatesAutoresizingMaskIntoConstraints to NO
_headerView.backgroundColor = [UIColor orangeColor];
_headerView.clipsToBounds = YES;
[_headerView addSubview:self.label];
[_headerView addSubview:self.deleteAllButton];
[_headerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[_deleteAllButton]-[_label]-|" options:NSLayoutFormatAlignAllCenterY metrics:0 views:NSDictionaryOfVariableBindings(_label, _deleteAllButton)]];
[_headerView addConstraint:[NSLayoutConstraint constraintWithItem:self.deleteAllButton attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_headerView attribute:NSLayoutAttributeCenterY multiplier:1.0f constant:0.0f]];
}
return _headerView;
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (self.editing) {
self.tableView.tableHeaderView = self.headerView;
[self.tableView layoutIfNeeded];
}
[UIView animateWithDuration:1.0 animations:^{
CGRect rect = self.headerView.frame;
if (editing) {
rect.size.height = 60.0f; // arbitrary; for testing purposes
} else {
rect.size.height = 0.0f;
}
self.headerView.frame = rect;
self.tableView.tableHeaderView = self.headerView;
[self.tableView layoutIfNeeded];
} completion:^(BOOL finished) {
if (!editing) {
self.tableView.tableHeaderView = nil;
}
}];
}
- (void)handleDeleteAll
{
NSLog(#"handle delete all");
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.mockData count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = self.mockData[indexPath.row];
return cell;
}
#end
Quite a time since you asked the question, but maybe the answer is jet helpfull to you (or others).
Autolayout has (automatically) added a constraint for the whole section header width (the last in the debug output constrains list). This should of course be no problem, as the width is taken into account when calculation the frames of the subviews.
But sometimes there seem to be rounding errors in the calculation of the frames...
Just add a lower priority to one of the subviews width values to solve the problem:
...#"|-[deleteAllButton(30.0#999)]-[currSizeText]-|"
If the button width is not constant use ...deleteAllButton(>=30#999)...
The workaround that I've tried using is to skip the section header stuff and go directly to the tableHeaderView. I've replaced my editing property with this:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (editing)
self.tableView.tableHeaderView = [self headerView];
else
self.tableView.tableHeaderView = nil;
}
It doesn't animate as nicely as the section header, but this will do for now.
This doesn't really address the actual problem (hence "workaround") so I won't accept this as the solution.