UIScrollView Autolayout prevent from scrolling vertically - uiscrollview

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.

Related

Auto Layout on OS X - make NSTextField fill superview

I can't get Auto Layout to work on OS X. What I'm trying to do is pretty simple, namely I have an NSTextField that is supposed to fill the entire space of its superview. Here's the minimal working example code I'm using:
#import AppKit;
int main() {
#autoreleasepool {
NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 300, 300)
styleMask:(NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask)
backing:NSBackingStoreBuffered
defer:NO];
NSTextField *textField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)];
textField.stringValue = #"Lorem ipsum dolor sit atmet.";
[window.contentView addSubview:textField];
textField.translatesAutoresizingMaskIntoConstraints = NO;
window.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[textField setContentHuggingPriority:1
forOrientation:NSLayoutConstraintOrientationHorizontal];
[textField setContentHuggingPriority:1
forOrientation:NSLayoutConstraintOrientationVertical];
[window.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[textField]-|"
options:0
metrics:nil
views:#{#"textField": textField}]];
[window.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[textField]-|"
options:0
metrics:nil
views:#{#"textField": textField}]];
window.contentView.wantsLayer = YES;
window.contentView.layer.borderWidth = 5;
[window makeKeyAndOrderFront:nil];
[[NSApplication sharedApplication] run];
[textField release];
[window release];
}
return EXIT_SUCCESS;
}
When I run this, the text field doesn't appear. When you resize the window, you can clearly see the border of the content view resizing properly. What am I doing wrong?
I believe you shouldn't alter NSWindow's contentView behavior, e.g. setting its translatesAutoresizingMaskIntoConstraints property.
Removing the following line works for me:
window.contentView.translatesAutoresizingMaskIntoConstraints = NO;

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

Aligning NSTextField and an Image

I am trying to align an NSTextField and an NSImageView.. My current code is below. I have tried a bunch of different approaches including subclassing NSTextFieldCell (found here on SO), messing around with the frame of the text field, and tweaking constraints, but I just can't get it.. No matter what I do, it looks like the screenshot below..
I also have discovered that when I don't apply a font to the label, alignment works as I would expect -- it is vertically aligned with the image.
So the question is, a) why in the world does applying a font screw up the alignment, and b) how do I get around this, ideally in a dynamic way that will adapt if i change the font at runtime..
- (id)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
NSView *spacer1 = [[NSView alloc] init];
[spacer1 setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:spacer1];
NSView *spacer2 = [[NSView alloc] init];
[spacer2 setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:spacer2];
NSImage *icon = [NSImage imageNamed:#"05-arrow-west"];
NSImageView *iconView = [[NSImageView alloc] init];
[iconView setTranslatesAutoresizingMaskIntoConstraints:NO];
[iconView setImage:icon];
[self addSubview:iconView];
NSFont *font = [NSFont fontWithName:#"HelveticaNeue-Light" size:14];
NSTextField *label = [[NSTextField alloc] init];
[label setTranslatesAutoresizingMaskIntoConstraints:NO];
[label setEditable:NO];
[label setSelectable:NO];
[label setBezeled:NO];
[label setDrawsBackground:NO];
[label setFont:font];
[label setTextColor:[NSColor lightGrayColor]];
[label setStringValue:#"Test String"];
[self addSubview:label];
NSDictionary *views = NSDictionaryOfVariableBindings(iconView, label, spacer1, spacer2);
[self addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[spacer1(>=0)][iconView]-5-[label][spacer2(==spacer1)]|"
options: 0
metrics:nil
views:views]];
[self addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[iconView]|"
options: 0
metrics:nil
views:views]];
[self addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[label]|"
options: 0
metrics:nil
views:views]];
[self setContentHuggingPriority:200 forOrientation:NSLayoutConstraintOrientationVertical];
[iconView setContentHuggingPriority:200 forOrientation:NSLayoutConstraintOrientationVertical];
[label setContentHuggingPriority:200 forOrientation:NSLayoutConstraintOrientationVertical];
}
return self;
}
This is a very common problem related to AppKit calculating incorrect metrics for certain fonts. Helvetica Neue and its variants are susceptible to this problem. Autolayout depends on the intrinsicContentSize of the NSTextField, which uses broken metrics to calculate the appropriate size to display the text. The only way I know of to work around this problem is to use magic offsets in your layout constraints to manually align the text.
In the end, this did it:
[self addConstraint:[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeBaseline
relatedBy:NSLayoutRelationEqual
toItem:iconView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0]];

Autolayout inside a view-based NSTableView

I have an NSTableView (view-based) that creates a row;
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
TaskTableCellView *tableCellView = [[TaskTableCellView alloc] init];
return tableCellView;
}
-(void) tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row {
NSView *view = [rowView viewAtColumn:0];
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *views = NSDictionaryOfVariableBindings(view);
[tableView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[view]|" options:0 metrics:nil views:views]];
[tableView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[view]|" options:0 metrics:nil views:views]];
}
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row {
return 20;
}
This row creates some subviews and assigns some constraints;
- (void)layout {
[super layout];
ViewWithBackground *viewWithBackground = [[ViewWithBackground alloc] init];
viewWithBackground.backgroundColor = [NSColor greenColor];
[self addSubview:viewWithBackground];
[viewWithBackground setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *views = NSDictionaryOfVariableBindings(viewWithBackground);
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[viewWithBackground]|"
options:0
metrics:nil
views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[viewWithBackground]|"
options:0
metrics:nil
views:views]];
[viewWithBackground setContentHuggingPriority:200 forOrientation:NSLayoutConstraintOrientationVertical];
[viewWithBackground setContentHuggingPriority:200 forOrientation:NSLayoutConstraintOrientationHorizontal];
}
- (void)drawRect:(NSRect)dirtyRect {
[[NSColor redColor] set];
NSRectFill(dirtyRect);
[super drawRect:dirtyRect];
}
The fun starts when I actually try to edit the constraints.. viewWithBackground is just an empty NSView that sets it's background. When the constraint is |[viewWithBackground]| for both horizontal and vertical, I get the expected result -- green rows. When I change it to the most basic |-[viewWithBackground]-|, I get a decidedly unexpected result -- red rows, and no sign of my green view!
Is there some additional step I'm supposed to take here? My goal is to have my viewWithBackground actually be a slightly smaller view, to fake the 'gaps' between rows and the spacing from the edges of the table view..
In case anyone ever stumbles on this.. it turns out that NSTableCellView is a bit wonky without a min size -- adding (>=10) on the Vertical constraint took care of the issue..

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

Resources