How do I use UIScrollView with AutoLayout (iOS6)? - uiscrollview

I am trying to build a view that will have several different controls, but I want the view to only scroll in the vertical direction. I'm using auto-layout so I don't want to have to specify the size manually (I know I can do that, but from what I understand according to the release notes on UIScrollView you should not have to).
So if I create a simple view controller (with no .xib file) and simply add the following to the init method I'd expect that my labels would wrap (which they do) and that my view would scroll vertically, however, this is not the case:
- (id)init {
self = [super init];
if (self) {
UIScrollView *_scrollView = [UIScrollView new];
[_scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
UILabel *label1 = [UILabel new];
UILabel *label2 = [UILabel new];
[label1 setTranslatesAutoresizingMaskIntoConstraints:NO];
[label2 setTranslatesAutoresizingMaskIntoConstraints:NO];
[label1 setNumberOfLines:0];
[label2 setNumberOfLines:0];
[label1 setPreferredMaxLayoutWidth:240];
[label2 setPreferredMaxLayoutWidth:240];
NSString *sampleText = #"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
[label1 setText:sampleText];
[label2 setText:sampleText];
[self.view addSubview:_scrollView];
[_scrollView addSubview:label1];
[_scrollView addSubview:label2];
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_scrollView)];
[self.view addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_scrollView)];
[self.view addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[label1]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(label1)];
[_scrollView addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[label2]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(label2)];
[_scrollView addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[label1]-[label2]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(label1, label2)];
[_scrollView addConstraints:constraints];
}
return self;
}
There are several results from this code that I think are odd. If you log the scrollviewheight in the viewDidLayoutSubviews method (after the super call), which is where all constraints should be applied, then the height is reporting 0. What's also strange is that the width is reporting a different number than I would expect. I would expect it to report the width of the superview due to the very first constraint (I even tried forcing the width inside that constraint with a priority that is supposedly required but it still didn't work).
As I understand it, the UIScrollView's content size should be set by the intrinsic sizes of its content but that doesn't seem to be happening.
So, does anyone know what is missing?

I had contacted Apple's support to get an answer on my question. The problem with the code is that the objects contained within the UIScrollView must be pinned to both the top and bottom vertically for it to determine its intrinsic content size. So the following line of code
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[label1]-[label2]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(label1, label2)];
[_scrollView addConstraints:constraints];
Should be changed to:
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[label1]-[label2]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(label1, label2)];
[_scrollView addConstraints:constraints];
They also pointed out that I was using the "new" keyword on several objects where the basic "init" is not the designated initializer. While that had nothing to do with my specific problem they did point out that you should avoid doing that as your object could be in an invalid state.

Related

How do I use NSLayoutConstraint to make a subview take up remaining/total height of superview

I have a superview to wich I add two subviews (subview1 and subview2).
I want subview1 to have same width as superview and stretch with superview and also have a height of 30px and align to the top of superview. This I can get working with the following code:
NSDictionary *metrics = #{#"height":[NSNumber numberWithFloat:30.0f]};
NSDictionary *views = NSDictionaryOfVariableBindings(subview1);
NSArray *tabContainerConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[subview1(==height)]" options:NSLayoutFormatAlignAllTop metrics:metrics views:views];
[superview addConstraints:tabContainerConstraints];
metrics = nil;
tabContainerConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|[subview1]|" options:(NSLayoutFormatAlignAllLeading | NSLayoutFormatAlignAllTrailing) metrics:metrics views:views];
[superview addConstraints:tabContainerConstraints];
Then I want subview2 to also have same width as superview and stretch with superview and also I want subview2 to align its top to subview1's bottom and then I want subview2 to fill all of the remaining height of superview (align bottom to bottom) and stretch in height with superview. I try to do this with this code:
NSDictionary *metrics = nil;
NSDictionary *views = NSDictionaryOfVariableBindings(subview2);
NSArray *tabContainerConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|[subview2]|" options:(NSLayoutFormatAlignAllLeading | NSLayoutFormatAlignAllTrailing) metrics:metrics views:views];
[superview addConstraints:tabContainerConstraints];
views = NSDictionaryOfVariableBindings(subview1, subview2);
tabContainerConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[subview1][subview2]-0-|" options:0 metrics:metrics views:views];
[superview addConstraints:tabContainerConstraints];
But strange things happens... subview is always aligned to with its top to subview1's bottom and also has the full width of superview so this is good. But height is strange... When first drawn/displayed subview2 has a very limited height, somewhere between 20-30 pixel it seems, but I can force a redraw by switching to another tab/view and back and then it is drawn in full/correct height. My subview2 is a NSTextView, and when I type in text strange things happen, my subview2 suddenly does not take up all height and is no longer aligned with the bottom of superview.
I hope my explanation is ok, if not please ask any question. Any ideas on how to fix this? I thought the |-0-[view]-0-| would do the trick?
Thank you
Søren
It's not clear from your question whether you're running all of that code or the first snippet is for one view only and the second snippet is for two views.
You should be combining the two VFL strings into one, and you don't need some of the layout options that you are supplying. If you only have one view in the VFL string, the layout options are meaningless.
I'd suggest the following code for creating your constraints:
NSDictionary *metrics = #{#"height":[NSNumber numberWithFloat:30.0f]};
NSDictionary *views = NSDictionaryOfVariableBindings(subview1,subview2);
// Horizontal layout for subview 1
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[subview1]|" options:0 metrics:metrics views:views]];
// Vertical layout - the options here mean that subview2 will be the same width as subview1
[superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[subview1(==height)][subview2]|" options:NSLayoutFormatAlignAllLeft | NSLayoutFormatAlignAllRight metrics:metrics views:views]];
You may be having some issues with the intrinsic size of the text view, if it behaves differently when it has content inside it or not. Or, the superview itself may be changing its size - what are the constraints on the superview? You will need to set some borders / background colours or use an introspection tool to determine which frames are going funny.

Auto layout, inserted view to fill the container view

I'am trying to make the inserted view to fill the container view in which it is inserted, however iam getting
Invalid parameter not satisfying: [constraint isKindOfClass:[NSLayoutConstraint class]]
- (void)insertedView:(NSView *)insertedView needsToFillContainerView:(NSView *)containerView {
[containerView addSubview:insertedView];
[containerView setTranslatesAutoresizingMaskIntoConstraints:NO];
[containerView addConstraints:#[
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[insertedView]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(insertedView)],
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[insertedView]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(insertedView)]
]
];
}
Basically the one view i want to insert is loaded from a VC
- (void)addSubview:(NSView *)insertedView fillingAndInsertedIntoView:(NSView *)containerView {
[containerView addSubview:insertedView];
[insertedView setTranslatesAutoresizingMaskIntoConstraints:NO];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[insertedView]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(insertedView)]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[insertedView]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(insertedView)]];
[containeView layoutIfNeeded];
}
constraintsWithVisualFormat returns an NSArray.
#[] is the Objective C literal to create an NSArray.
So, here your method parameter for addConstraints is an NSArray with two elements, each of which is an NSArray.
That's an incorrect method parameter for addConstraints. It expects an NSArray of objects of type NSLayoutConstraint.
Changing your invocation to be along the lines of this will solve the problem:
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat: etc.
Incidentally, I see so many posts about creating Auto Layout constraints in code. Interface Builder is far the superior method. As Erica Sadun so succinctly puts it in her book iOS Auto Layout Demystified:
Any views you lay out in Interface Builder are guaranteed to be satisfiable. You cannot create a wrong interface with inconsistent rules in IB. The same is not true in code.
There are two methods in UIView
//parameter type is NSArray
-(void)addConstraints:(NSArray *) constraints
and
// parameter type is NSLayoutConstraint
-(void)addConstraint:(NSLayoutConstraint *) constraint
So,
constraintsWithVisualFormat
will return a NSArray, you need use
addConstraints

How do I resize a textview and scrollview content size based on dynamic content

I have a scrollview that contains an image and a single textview. The content of the textview is dynamic it could be 200 in height or it could be 1500 in height. I am using storyboard so how do I setup both the textview and the scrollview content size to allow for dynamic content in the textview. I can set them both to arbitrarily high number but then I end up with a bunch empty space in the scrollview. I want both the image and the text to scrolling on and off the screen as required.
You would use constraints. Here is an example of how I did it with a scrollview. With a textview it is scrollable, and doesn't require you to set a content size, you simply put the content in it. If you want to control the viewable area of the uitextview, you will need to adjust it to what you see fit based on the amount of text you have, font and spacing you use.
NSDictionary *viewsDic = #{
#"image":image,
#"text": text,
#"lable":label,
#"someview":someview,
#"anotherview":anotherview};
[someview addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[image(320)]|"
options:0
metrics:0
views:viewsDic]];
[someview addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[image]|"
options:0
metrics:0
views:viewsDic]];
[someview addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-14-[label]"
#"[text(<=200)]"
options:0
metrics:0
views:viewsDic]];
[someview addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-22-[txtDollarAmountFront(==37)]"
options:0
metrics:0
views:viewsDic]];
[someview addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-19-[label]"
options:0
metrics:0

NSTableView's frame inside a NSClipView/NSScrollView using auto layout

I'm trying to create a NSTableView inside a NSScrollView (the standard configuration, that is) in code, using auto layout. I can't figure out how to make this work.
Here's my loadView:
- (void)loadView
{
NSView *view = [[NSView alloc] init];
NSScrollView *tableScroll = [[NSScrollView alloc] init];
NSTableView *fileTable = [[NSTableView alloc] init];
[tableScroll setDocumentView:fileTable];
[tableScroll setHasVerticalScroller:YES];
[tableScroll setHasHorizontalScroller:NO];
fileTable.delegate = self;
fileTable.dataSource = self;
[fileTable setHeaderView:nil];
[fileTable setAllowsColumnReordering:NO];
NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:#"column1"];
[fileTable addTableColumn:column];
[tableScroll setTranslatesAutoresizingMaskIntoConstraints:NO];
[fileTable setTranslatesAutoresizingMaskIntoConstraints:NO];
[view addSubview:tableScroll];
NSDictionary *topViews = NSDictionaryOfVariableBindings(tableScroll);
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[tableScroll]|" options:0 metrics:nil views:topViews]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[tableScroll]|" options:0 metrics:nil views:topViews]];
self.fileTable = fileTable;
self.view = view;
}
What happens is that my table view's frame will be always equal to the bounds of the NSClipView. The view is inside a window and gets resized with it, and when I do that it'll resize the scrollview, the clip view and the table, but I can never scroll anywhere.
Looking at constraints I get, the NSScrollView gets constraints that set the clip view to fill it, the clip view has no constraints at all and the table view has a bunch of constraints related to the NSTableRowViews inside it.
If I add a constraint like |[fileTable(>=500)] to the clip view I'll get 500 pixels of NSTableView, but obviously I don't want to do that.
Even though this was answered by the poster in the comments above, I thought I’d put the answer here (having run into the same issue). If you are adopting auto layout, you would typically uncheck “Translates Mask Into Constraints” in the xib. However, for classes like NSScrollView and NSTableView, you should generally let them manage their own internal views by setting their translatesAutoresizingMaskIntoConstraints property to YES. It is still ok to set constraints that are external to these views, i.e. to resize in relation to their superview.
If you set translatesAutoresizingMaskIntoConstraints to NO, then you will need to supply constraints for all of the internal views, which unless you specifically need custom behavior (almost never), you will not want to do. This was the specific problem above.
An obvious side effect of not setting this correctly is that a table (for example) will not properly scroll beyond what is visible in the view.

In OSX 10.8 how do I constrain a subview to be the same size as its parent view

I have the default NSWindow created in a new application which has a single NSView. I then create a new NSViewController which has it's own XIB and a view. In the app delegate I do the obvious
self.mainViewController = [[MainViewController alloc] initWithNibName:#"MainViewController" bundle:nil];
[self.window.contentView addSubview:self.mainViewController.view];
self.mainViewController.view.frame = ((NSView*)self.window.contentView).bounds;
OK, how do I set a constraint in the new way to have my subview keep its size identical to the Window, i.e. it's superview. It doesn't seem to work automatically. Autoresizessubviews is ON for both views.
Basically, you need to constrain four things:
The leading space of your subview to its superview to be zero
The top space of your subview to its superview to be zero
The width of your subview to be equal to its superview's width
The height of your subview to be equal to its superview's width
If the visual constraint isn't working out for you, you can build these four constraints individually in code. Use the method +constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:
constant: to specify exact relationships between different views' attributes. For example, constraint #1 above might be expressed by:
[NSLayoutConstraint constraintWithItem:mySubview
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:mySuperview
attribute:NSLayoutAttributeLeading
multiplier:1.0f
constant:0.0f]
and #3 might be:
[NSLayoutConstraint constraintWithItem:mySubview
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:mySuperview
attribute:NSLayoutAttributeWidth
multiplier:1.0f
constant:0.0f]
Once you've built up those four constraints, you can add them to your superview as needed.
Note that there are multiple ways to achieve the same effect as above:
You might constrain the trailing space and bottom space instead of the width and height
You might constrain the center X and center Y instead of the leading and top spaces
You can also probably come up with the same constraints in a visual representation, as in Peter Hosey's answer. For example, an equal-width constraint might look like #"[mySubview(==mySuperview)]" with the appropriate views dictionary.
Keep in mind that the Auto Layout Guide is a wealth of information about constraints, including how to debug them when things go wrong.
In the nib editor, drag the subview's size until it is the same size as its superview. Xcode will create an appropriate width constraint automatically.
In code, I would try |-0-[mySubview]-0-| (adapted from the example in the constraint syntax documentation).
Just like Peter wrote, you should use the visual format language.
When doing this, however, order is important: when you create a constraint, all views it references have to be part of the same view hierarchy.
So given your example, the code would have to become:
self.mainViewController = [[MainViewController alloc] initWithNibName:#"MainViewContoller" bundle:nil];
NSView *containerView = self.window.contentView;
NSView *contentView = self.mainViewController.view;
[contentView setTranslatesAutoresizingMaskIntoConstraints: NO];
[containerView addSubview:contentView];
NSDictionary *viewBindings = NSDictionaryOfVariableBindings(contentView);
[containerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[contentView]|" options:0 metrics:nil views:viewBindings]];
[containerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[contentView]|" options:0 metrics:nil views:viewBindings]];
You can override setContentView:, contentView:, and contentRectForFrameRect: so they will deal with window.frame - sized view.
If you're ok with using a 3rd party library, you can accomplish this with ReactiveCocoaLayout in one simple line:
RAC(view,rcl_frame) = parentView.rcl_frameSignal;
I had the same problem and I ended up with this solution which is working with SDK 10.10. Just set the autoresizingMask of the new view to be the same as the parent window. Only one row of code and it works like a charm...
self.masterViewController = [[MasterViewController alloc] initWithNibName:#"MasterViewController" bundle:nil];
[self.window.contentView addSubview:self.masterViewController.view];
self.masterViewController.view.frame = ((NSView *)self.window.contentView).bounds;
self.masterViewController.view.autoresizingMask = ((NSView *)self.window.contentView).autoresizingMask;

Resources