Programmatically implementing two different layouts using size classes - ios8

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) {
...
}
}

Related

Autolayout in an NSView being used as a cell for NSTableView

I am trying to build an NSTableView similar to how Things does it's layout.
I assume what they are doing is using an NSTableView with a custom-drawn NSTableCell -- or perhaps it's an NSSegmentedControl. I am attempting to go down the NSTableCell route. I attempted to subclass an NSTableCellView and draw a custom cell (this is all in the init method for testing);
- (id)init {
self = [super init];
if (self) {
_checkbox = [[NSButton alloc] init];
[_checkbox setButtonType:NSSwitchButton];
_textview = [[NSTextView alloc] init];
[self addSubview:_checkbox];
[self addSubview:_textview];
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *views = NSDictionaryOfVariableBindings(_checkbox, _textview);
[self addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_checkbox]-[_textview]|"
options:0
metrics:nil
views:views]];
}
return self;
}
#end
Seems pretty self-explanatory, but it doesn't actually work. I am getting errors on constraints not being able to be satisfied. Is it not possible to use autolayout inside a subclassed NSTableCellView?
It is possible although you have to introduce some additional code to the table view controller:
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
NSTableCellView *rowView = [self.tableView makeViewWithIdentifier: RVIssueSelectionTableRowViewContentKey owner: self];
[rowView setObjectValue: yourDataObject]; // or update your cell with the according data the way you prefer
[rowView setNeedsLayout: YES];
[rowView layoutSubtreeIfNeeded];
return [rowView fittingSize].height;
}
This will cause the cell to update it's layout and you can return it's desired height. Because this call can be kind of expensive you should cache the calculated cell height.
This answer has been taken from another SO answer which I could not find right now (will update the solution when I've found it).
Your error messages for the constraints are generated because you force the cell to have a height of x (through the table view datasource methods). But your constraints want to set the height of the cell so they are satisfied. Both at the same time can not work.

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.

Cocoa autolayout constraint - programmatic padding of variable number of views

I want to be able to add new views to a superview but so that they keep a constant vertical distance between each other. For that I tried to programmatically set up a constraint for each view but I could not figure out how to do it. The problem is I do not know beforehand the number or the relative position of the views.
Is there a way to programmaically set up a constraint for each view so that regardless of whatever other views they neighbor, autolayout will keep the constant spacing between the views?
Possible this short code snippet is what you are looking for:
NSMutableArray* newVerticalConstraints = [NSMutableArray array];
UIView* firstView = nil;
UIView* secondView = nil;
UIView* superview = <Your container view>;
NSArray* subviews = [superview subviews];
if ([subviews count] > 0) {
firstView = [subviews objectAtIndex:0];
// Add first constraint
[newVerticalConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[firstView]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(firstView)]];
for (int i = 1; i < [subviews count]; i++) {
secondView = [subviews objectAtIndex:i];
[newVerticalConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[firstView]-10-[secondView]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(firstView,secondView)]];
firstView = secondView;
}
// Add last constraint
[newVerticalConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[firstView]-10-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(firstView)]];
[superview removeConstraints:self.verticalConstraints];
[superview addConstraints:newVerticalConstraints];
// Save all vertical constraints to be able to remove them
self.verticalConstraints = newVerticalConstraints;
}

UIScrollView + Autolayout issue

Considering the following structure:
| UIScrollView | UITextField | (UIScrollView on the left and a UITextField the right)
I am trying to add a UILabel into the UIScrollView and make the UIScrollView grow according to the UILabel size. The UITextFiled would then get smaller according to the grow of the UIScrollView.
- (IBAction)makeScrollViewGrow:(id)sender
self.myScrollView.translatesAutoresizingMaskIntoConstraints = NO;
self.myTextField.translatesAutoresizingMaskIntoConstraints = NO;
UILabel *myLabel = [[UILabel alloc] init];
myLabel.text = #"THIS IS A LONG MESSAGE";
myLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.myScrollView addSubview:myLabel];
UITextField *theTextField = self.myTextField;
[self.myTextField removeConstraint:self.titleTextFieldWidthConstraint];
[self.myScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[myLabel]|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(myLabel)]];
[self.myScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[myLabel]|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(myLabel)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-20-[theSCrollView]-8-[theTextField]-20-|" options:0 metrics: 0 views:NSDictionaryOfVariableBindings(theSCrollView, theTextField)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[theSCrollView]|" options:0 metrics: 0 views:NSDictionaryOfVariableBindings(theSCrollView)]];
}
The issue here is that the UIScrollView is not being resized because the UITextField has a fixed width. If I remove programatically the width UITextField constraint the UIScrollView takes over the all width without considering the text Label (intrinsicContentSize).
Will I need to set the Scrollview contentsize manually or can I do this automatically using autolayout?
Thanks

Auto layout of custom programmatic UITableViewCell fails upon scrolling

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];
}
}

Resources