Auto layout of custom programmatic UITableViewCell fails upon scrolling - xcode

I'm trying to use the new auto layout capability of iOS 6 on a custom UITableViewCell which has been implemented programmatically. I added the addConstraint calls, and it works properly at first-- until I scroll. When I come back to the cell after scrolling the layout is trashed. By trashed I mean the margins between fields are all wrong (too large, well beyond the size of the cell). I'm speculating this has something to do with the dequeueReusableCellWithIdentifier method leaving me with a "dirty" cell, the same way you find yourself needing to reinitialize fields within cells, but I can't seem to do anything to coax it to render properly again. I've tried calling [self.contentView updateConstraints] before returning the cell. I've tried destroying the constraints and recreating them. Not only does it not work, but if it's attempted in layoutSubviews it freezes in an endless loop of some kind. Any ideas?
Here's the code to establish the constraints. It's located in initWithStyle:reuseIdentifier:
[self.completedLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.nextSetHeaderLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.nextSetDetailLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.youWillLearnHeaderLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.youWillLearnDetailLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.contentView removeConstraints:[self.contentView constraints]];
NSDictionary *views = NSDictionaryOfVariableBindings(_completedLabel, _nextSetHeaderLabel, _nextSetDetailLabel, _youWillLearnHeaderLabel, _youWillLearnDetailLabel);
[self.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[_completedLabel]-5-|"
options:0
metrics:nil
views:views]];
[self.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[_nextSetHeaderLabel]-5-|"
options:0
metrics:nil
views:views]];
[self.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[_nextSetDetailLabel]-5-|"
options:0
metrics:nil
views:views]];
[self.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[_youWillLearnHeaderLabel]-5-|"
options:0
metrics:nil
views:views]];
[self.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-5-[_youWillLearnDetailLabel]-4-|"
options:0
metrics:nil
views:views]];
[self.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-5-[_completedLabel]-12-[_nextSetHeaderLabel]-0-[_nextSetDetailLabel]-12-[_youWillLearnHeaderLabel]-0-[_youWillLearnDetailLabel(>=20)]-1-|"
options:0
metrics:nil
views:views]];

I ran into this issue as well. If I wasn't dequeuing cells, everything seemed to work - scrolling, rotation etc. However, if I dequeued cells, then the layout started getting messed up. The only way I could get it to work was by overriding the cell's prepareForReuse method. In this method,
1. remove all the custom subviews
2. remove all constraints associated with those subviews from contentView
3. add subviews and constraints again
-(void) prepareForReuse
{
[self removeCustomSubviewsFromContentView];
[self.contentView removeConstraints:self.constraints] //self.constraits holds all the added constraints
[self setupSubviewsInContentView];
[self addConstraintsToContentView];
}
If there is a better way to do this, I would love to learn as well :) I believe the advantage of dequeing is that the tableView does not have to hold a large number of cells in memory - but, with this method, one has to go through the cost of essentially setting up the cell everytime you dequeue.

I had a similar problem, in case anyone is interested I've found a solution, see this question
What I've done:
- (void)awakeFromNib
{
[super awakeFromNib];
for (NSLayoutConstraint *cellConstraint in self.constraints)
{
[self removeConstraint:cellConstraint];
id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
NSLayoutConstraint* contentViewConstraint = [NSLayoutConstraint constraintWithItem:firstItem
attribute:cellConstraint.firstAttribute
relatedBy:cellConstraint.relation
toItem:seccondItem
attribute:cellConstraint.secondAttribute
multiplier:cellConstraint.multiplier
constant:cellConstraint.constant];
[self.contentView addConstraint:contentViewConstraint];
}
}

Related

visual format constraints in iOs 8

I'm trying to add visual contraints to my view. In iOS 7 everything works fine but with 8.0 no.
My code
NSDictionary *views = NSDictionaryOfVariableBindings(_view1, _view2);
[self.view removeConstraints:self.view.constraints];
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[_view1]-10-|" options:0 metrics:nil views:views];
constraints = [constraints arrayByAddingObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[_view2(200.0)]" options:0 metrics:nil views:views]];
constraints = [constraints arrayByAddingObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-50-[_view1(200.0)]-10-[_view2(200)]" options:0 metrics:nil views:views]];
[self.view addConstraints:constraints];
The view1 after apply contraints has 0 width.
I've searched for apple docs but nothing referes that this method is deprecated.
Any ideas?

Programmatically implementing two different layouts using size classes

I have a four buttons layout. In portrait they should be shown one above the other. In landscape they should be in two columns each with two buttons.
I implement the buttons in code - really simple stuff:
UIButton *btn1 = [[UIButton alloc] init];
[self.view addSubview: btn1];
UIButton *btn2 = [[UIButton alloc] init];
[self.view addSubview: btn2];
UIButton *btn3 = [[UIButton alloc] init];
[self.view addSubview: btn3];
UIButton *btn4 = [[UIButton alloc] init];
[self.view addSubview: btn4];
NSDictionary *views = NSDictionaryOfVariableBindings(btn1, btn2, btn3, btn4);
[btn1 setTranslatesAutoresizingMaskIntoConstraints:NO];
[btn2 setTranslatesAutoresizingMaskIntoConstraints:NO];
[btn3 setTranslatesAutoresizingMaskIntoConstraints:NO];
[btn4 setTranslatesAutoresizingMaskIntoConstraints:NO];
// portrait constraints
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(50)-[btn1]-(50)-|"
options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(50)-[btn2]-(50)-|"
options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(50)-[btn3]-(50)-|"
options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(50)-[btn4]-(50)-|"
options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[btn1]-[btn2]-[btn3]-[btn4]-(50)-|"
options:0 metrics:nil views:views]];
This is obviously the setup for portrait layout. I would used to have determined the device and its orientation to make specific case for iPad and iPhone in there respective orientations. But now we are supposed to use size classes. How can I determine if the size class is "compact"... and thus set the appropriate constraints?
In the meantime I have found a good solution. Since this question has so many upvotes, I thought I would quickly describe it. I was inspired to this solution by a WWDC session.
I have moved on to Swift so please excuse that the code will be in swift - the concept is however the same for Obj-C.
You start out by declaring three constraint arrays:
// Constraints
private var compactConstraints: [NSLayoutConstraint] = []
private var regularConstraints: [NSLayoutConstraint] = []
private var sharedConstraints: [NSLayoutConstraint] = []
And then you fill the constraints accordingly. You can i.e. do this in a separate function that you call from viewDidLoad or you do it in viewDidLoad directly.
sharedConstraints.append(contentsOf: [
btnStackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
...
])
compactConstraints.append(contentsOf: [
btnStackView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7),
...
])
regularConstraints.append(contentsOf: [
btnStackView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.4),
...
])
The important part is switching between the size classes and activating/deactivating the appropriate constraints.
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if (!sharedConstraints[0].isActive) {
// activating shared constraints
NSLayoutConstraint.activate(sharedConstraints)
}
if traitCollection.horizontalSizeClass == .compact && traitCollection.verticalSizeClass == .regular {
if regularConstraints.count > 0 && regularConstraints[0].isActive {
NSLayoutConstraint.deactivate(regularConstraints)
}
// activating compact constraints
NSLayoutConstraint.activate(compactConstraints)
} else {
if compactConstraints.count > 0 && compactConstraints[0].isActive {
NSLayoutConstraint.deactivate(compactConstraints)
}
// activating regular constraints
NSLayoutConstraint.activate(regularConstraints)
}
}
I know that the constraints don't fit to the ones in the question. But the constraints themselves are irrelevant. The main thing is how one switches between two sets of constraints based on the size class.
Hope this helps.
You can examine the view's trait collection to determine its horizontal and vertical size class.
if (self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
...
}
Implement the traitCollectionDidChange: method to automatically be called when a trait changes due to autorotation.
For more information, see UITraitCollection Class Reference and UITraitEnvironment Protocol Reference.
Swift 4 code for the accepted answer:
if (self.view.traitCollection.horizontalSizeClass == .compact) {
...
}
traitCollection only works in iOS8. So your app will crash on iOS7. Use the code below to support both iOS7 and iOS8
if ([self.view respondsToSelector:#selector(traitCollection)]){
if (self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
...
}
}

How to place 2 NSTextView vertically aligned with a space in between, and make container view resize accordingly with auto layout

Hope the title is clear. Trying to have something below
--------------------
| [titleTextView] |
| | |
| [detailsTextView]|
--------------------
With the code that I tried, the container resized, but both titletextView and detailsTextView are placed together (overlapping each others). I know I init both at (16,0) but shouldn't the constrain place them correctly?
I also get the following error: Unable to simultaneously satisfy constraints:
("<NSLayoutConstraint:0x60000008fcd0 NSTextView:0x600000134be0.bottom == NSView:0x600000134c80.bottom + 20>",
"<NSAutoresizingMaskLayoutConstraint:0x608000093ce0 h=--& v=--& V:[NSTextView:0x600000134be0]-(0)-| (Names: '|':NSView:0x600000134c80 )>")
Code:
//title textView
self.titleTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(16, 0, [self.view frame].size.width - 30, 0)];
[self.titleTextView setEditable:NO];
[self.titleTextView setBackgroundColor:[NSColor clearColor]];
[self.titleTextView setString:#"potentially long text."];
[self.titleTextView setHorizontallyResizable:NO];
[self.titleTextView sizeToFit];
//detail textView
self.detailsTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(16, 0, [self.view frame].size.width - 30, 0)];
[self.detailsTextView setEditable:NO];
[self.detailsTextView setBackgroundColor:[NSColor clearColor]];
[self.detailsTextView setString:#"Very long text."];
[self.detailsTextView setHorizontallyResizable:NO];
[self.detailsTextView sizeToFit];
//Adding to self.view
[self.view addSubview: self.titleTextView];
[self.view addSubview: self.detailsTextView];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.titleTextView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.detailsTextView
attribute:NSLayoutAttributeTop
multiplier:1
constant:20]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.titleTextView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1
constant:20]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.detailsTextView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1
constant:20]];
When working with autolayout, you don't want to think about the layout in terms of frames at all. The constraints will determine the frame of your views.
Also, if you're creating the layout in code, you have to call setTranslatesAutoresizingMaskIntoConstraints:NO for the views which you want the autolayout engine to apply to.
So, you'd want to do something like:
UIView* titleTextView = [[UIView alloc] init];
[titleTextView setTranslatesAutoresizingMaskIntoConstraints:NO];
UIView* detailsTextView = [[UIView alloc] init];
[detailsTextView setTranslatesAutoresizingMaskIntoConstraints:NO];
...additional setup stuff...
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[titleTextView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(titleTextView)]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[detailsTextView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(detailsTextView)]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[titleTextView][detailsTextView]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(titleTextView, detailsTextView)]];
If working in code, I'd strongly recommend checking out the visual format language which will make setting up constraints much more efficient in code, but you can also do the same thing as above using individual constraints.
Forget all you've done earlier with rects and start thinking in relative positions.
Using my favorite category for autolayout:
https://github.com/jrturton/UIView-Autolayout
you can achieve what you want with these simple constraints (which I find MUCH more readable and intuitive than any of the official API solutions):
[self.titleTextView pinToSuperviewEdges:JRTViewPinLeftEdge|JRTViewPinTopEdge inset:20.0];
[self.detailsTextView pinToSuperviewEdges:JRTViewPinLeftEdge inset:20.0];
[self.detailsTextView pinEdge:NSLayoutAttributeTop toEdge:NSLayoutAttributeBottom ofView:self.titleTextView inset:20];
This will pin both textviews 20 pixels from the left, titleTextView 20 pixels from the top and detailsTextView 20 pixels below titleTextView. Also, the category will add the constraints to the correct view in each case.

Using Autolayout with NSScrollview

My application contains a main container view in which three different views(subviewA, subviewB and subviewC) are swapped in and out. Both subviewA and subviewB are to resize with the window and I do this as follows
NSView *myCurrentView = [_myCurrentViewController view];
if ([[_myCurrentViewController title] isEqualToString:subviewA] ||
[[_myCurrentViewController title] isEqualToString:subViewB]) {
[_myScrollView setHidden:YES];
[myCurrentView setTranslatesAutoresizingMaskIntoConstraints:NO];
[_mainWindowView addSubview: [_myCurrentViewController view]];
[_mainWindowView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[myCurrentView]|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(myCurrentView)]];
[_mainWindowView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[myCurrentView]|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(myCurrentView)]];
}
This works fine, however for subviewC i would like to have the horizontal size of the subview, resize with the window, but use scrolling for the vertical. So, in IB I added a NSScrollView as a subview of my mainWindowView. I hide the scroll view if subViewA or subviewB are selected, but I don't know how to properly configure the constraints for subviewC. I figured I could add horizontal constraints simliar to subviewA or subviewB, since I woulk like the view to resize with the window, however I don't know what to add for constraints in the vertical to allow scrolling, so far I have this
[[_myCurrentViewController view] setTranslatesAutoresizingMaskIntoConstraints:YES];
[_myScrollView setHidden:NO];
[_myScrollView setDocumentView:[_myCurrentViewController view]];
[_myScrollView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[myCurrentView]|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(myCurrentView)]];
Thanks

UIScrollView Autolayout prevent from scrolling vertically

I'm wondering how to crop image inside UIscrollView with autolayout
I'm trying to make UIscrollView scroll only horizontally. if image is higher than view height it should be cropped. I've tried a lot properties but can't make all images inside uiscrollview same height as view to avoid scrolling vertically.
Do i miss something?
#import "WelcomeController.h"
#interface WelcomeController ()
#property (strong, nonatomic) UIScrollView *scrollView;
#property (nonatomic,strong) NSArray *contentList;
#end
#implementation WelcomeController
#synthesize contentList =_contentList;
- (void)updateUI
{
UIScrollView* sv = self.scrollView;
id previousLab = nil;
for (UIView *lab in _contentList) {
lab.translatesAutoresizingMaskIntoConstraints = NO;
lab.backgroundColor = [UIColor blackColor];
lab.contentMode = UIViewContentModeScaleAspectFit;
[sv addSubview:lab];
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[lab]|"
options:0 metrics:nil
views:#{#"lab":lab}]];
if (!previousLab) { // first one, pin to top
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[lab]"
options:0 metrics:nil
views:#{#"lab":lab}]];
} else { // all others, pin to previous
[sv addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:#"H:[prev][lab]"
options:0 metrics:nil
views:#{#"lab":lab, #"prev":previousLab}]];
}
previousLab = lab;
}
// last one, pin to bottom and right, this dictates content size height
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[lab]|"
options:0 metrics:nil
views:#{#"lab":previousLab}]];
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:[lab]|"
options:0 metrics:nil
views:#{#"lab":previousLab}]];
}
-(void)setContentList:(NSArray *)contentList
{
_contentList = contentList;
[self updateUI];
}
- (void)setupScrollView
{
UIScrollView* sv = [UIScrollView new];
sv.backgroundColor = [UIColor redColor];
sv.translatesAutoresizingMaskIntoConstraints = NO;
sv.pagingEnabled = YES;
sv.showsHorizontalScrollIndicator =NO;
sv.showsVerticalScrollIndicator = NO;
sv.bounces =NO;
[self.view addSubview:sv];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[sv]|"
options:0 metrics:nil
views:#{#"sv":sv}]];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[sv]|"
options:0 metrics:nil
views:#{#"sv":sv}]];
self.scrollView = sv;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupScrollView];
//for testing
UIImageView *image1=[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"welcome1.jpg"]];
UIImageView *image2=[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"welcome2.jpg"]];
UIImageView *image3=[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"welcome3.jpg"]];
self.contentList = [NSArray arrayWithObjects:image1,image2,image3,nil];
}
#end
Have you tried setting the height of the scroll view explicitly?
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_sv(123)]" options:0 metrics:nil views:views]];
You would need to replace the height above (123) with the height you need, obviously, as well as views.
Autolayout automatically manages the UIScrollView's contentSize (the scrollable area). So if you stick a subview in there with an intrinsic size that is larger than the height, it will increase the contentSize. I can think of two things:
Stick images in a plain UIView with the same height as the scrollview.
Subclass the UIImageView and override the intrinsicContentSize method to return a fixed height for all images. This seems like a poor solution, though.
I think you should be able to set the image view's frames to a fixed height (via constraints). Then add constraints to have the image views top and bottom fixed with x constant from the scrollview.
This will let the scrollview know its exact content size to use. As long as its frame, then, (determined by whatever constraints you give it in relation to its superview) is >= the image view's fixed heights, it won't scroll vertically.

Resources