I am creating a custom keyboard on iOS using Objective-C. To increase the height of the keyboard to make it same as default keyboard height (with suggestions bar) for both portrait and landscape, I have added height constraints programatically { I removed the previously added constraint before adding the new one }.
I am updating the height in viewWillLayoutSubviews method of the class so that on orientation change, the desired height of the keyboard is achieved and it is working perfectly fine.
But my issue is that on orientation change, the view does not autolayout perfectly as it does in default keyboard on iOS, rather the view changes and then updates the height and then expands to the right of the screen to cover the complete area. ( basically a jerk happens before the keyboard completely loads itself )
Also, on initialization of the keyboard, the view is loaded with the default height and then it updates the custom height. Post that, it applies the auto layout constraint and updates the UI, hence providing the complete layout of the keyboard screen. The whole process looks a bit jerky.
I am using xib with auto layout.
In case I do not update the custom height of the keyboard, the above mentioned jerks are solved but the height of the keyboard remains default allowed by apple.
There is another thing that there are a lot of views in XIB and hence a lot of constraints. If I remove the constrains, the keyboard loads pretty quick and the custom height is also achieved. But as soon as I rotate the device, the view does not layouts and I am stuck with half keyboard on the screen.
Any suggestion would be great!
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// Perform custom initialization work here
if (IS_IPAD) {
self.portraitHeight = 300;
self.landscapeHeight = 300;
}else{
self.portraitHeight = 257;
self.landscapeHeight = 203;
}
}
return self;
}
- (void)updateViewConstraints {
[super updateViewConstraints];
// Add custom view sizing constraints here
if (_viewWillAppearExecuted)
[self adjustHeight];
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:YES];
[self.view addConstraint:self.heightConstraint];
_viewWillAppearExecuted = YES;
}
#pragma mark - Setters/Getters
- (NSLayoutConstraint *)heightConstraint
{
if (!_heightConstraint) {
_heightConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:self.portraitHeight];
_heightConstraint.priority = UILayoutPriorityRequired - 1;
}
return _heightConstraint;
}
#pragma mark - Methods
- (void)adjustHeight
{
if (self.view.frame.size.width == 0 || self.view.frame.size.height == 0)
return;
[self.view removeConstraint:self.heightConstraint];
// [self.imageView removeConstraint:self.heightConstraint];
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
CGFloat screenH = screenSize.height;
CGFloat screenW = screenSize.width;
BOOL isLandscape = !(self.view.frame.size.width ==
(screenW*(screenW<screenH))+(screenH*(screenW>screenH)));
self.isLandscape = isLandscape;
if (isLandscape) {
self.heightConstraint.constant = self.landscapeHeight;
// [self.imageView addConstraint:self.heightConstraint];
[self.view addConstraint:self.heightConstraint];
} else {
self.heightConstraint.constant = self.portraitHeight;
// [self.imageView addConstraint:self.heightConstraint];
[self.view addConstraint:self.heightConstraint];
}
}
Try this codes for your problem. First of all do some R&D with this code and let me know.
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'm trying to implement a growing UITextView with the content size, I am using this class and autolayout and it works great but my problem is that my inner scroll view is not scrolling.
It was scrolling before adding the methods for growing text view in viewDidLoad.
This is what is in my xib:
View
outerScrollView
innerScrollView
labels
Growing UITextView
I do this to switch scrolling between scrollview
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:self.innerScrollview]){
self.outerScrollview.scrollEnabled = NO;
self.innerScrollview.scrollEnabled = YES;
}
else{
self.outerScrollview.scrollEnabled = YES;
self.innerScrollview.scrollEnabled = NO;
}
}
and in viewDidLoad where the "msg" is the growing textview:
self.msgHandler = [[GrowingTextViewHandler alloc]initWithTextView:self.msg withHeightConstraint:self.msgHeight];
[self.msgHandler updateMinimumNumberOfLines:1 andMaximumNumberOfLine:INT_MAX];
and in textViewDidChange
- (void)textViewDidChange:(UITextView *)textView {
[self.msgHandler resizeTextViewWithAnimation:YES];
}
so why is the growing text affecting the scrolling of the inner scrollview?
int outerContentHeight = self.outer_last_label.frame.size.height + self.outer_last_label.frame.origin.y;
int innerContentHeight = self.inner_last_label.frame.size.height + self.inner_last_label.frame.origin.y;
[self.outerSrollview setContentSize:CGSizeMake([[UIScreen mainScreen] bounds].size.width,outerContentHeight)];
[self.innerScrollview setContentSize:CGSizeMake(self.scrollview2.frame.size.width, innerContentHeight)];
I have managed to fix it but I removed the use of the class"GrowingTextViewHandler" and I did this instead
I changed "scrollViewDidScroll" to this:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:self.innerScrollview]){
[self.view insertSubview:self.innerScrollview atIndex:0];
}
else{
[self.view insertSubview:self.outerScrollview atIndex:0];
}
}
and changed "textViewDidChange" to this
- (void)textViewDidChange:(UITextView *)textView {
float height = self.msg.contentSize.height;
[UITextView beginAnimations:nil context:nil];
[UITextView setAnimationDuration:0.5];
self.msgHeightConstraint.constant = height + 10.0; //Give it some padding
[UITextView commitAnimations];
}
Please set the content size of the scroll view, Do not change anything in the code, and check,
[self.scrollViewOuter setContentSize:CGSizeMake(200, 1000)];
[self.scrollViewInner setContentSize:CGSizeMake(200, 1000)];
My both the scroll views are working if, I add content size in it.
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 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.
I have a view in scrolling view, and when I seque(push) from this ViewController to CommentViewController, and came back, view in scroll view moved for 200 px up, I dont know why, I don`t anything in code.
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[scroller setScrollEnabled:YES];
[scroller setContentSize:CGSizeMake(320, 2000)];
}
Its when Auto Layout is on. When it off, its work, but design is not impressive...
Can I fix it with Auto Layout?
Here's a quick fix
In viewWillDisappear, save the scrollView's contentOffset and reset it to zero.
Later in viewDidLayoutSubview, restore the contentOffset to old value.
Here's the sample code
-(void) viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if(!CGPointEqualToPoint(CGPointZero, self.contentOffset))
{
scroller.contentOffset = self.contentOffset;
self.contentOffset = CGPointZero;
}
}
-(void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.contentOffset = scroller.contentOffset;
scroller.contentOffset = CGPointZero;
}