Resize NSView to its subview using auto layout - objective-c

In Interface Builder I've added a NSScrollView with a NSImageView, some labels, a horizontal separator and a NSView. The NSScrollView looks like the following:
Every element has constraints from the top left, and both the separator and the custom view also have a constraints to the right.
Sometimes I need to change the content of the custom view. I do this with the following code (options is the custom view, view is the view I want to display):
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
options.subviews = [NSArray arrayWithObject:view];
// Fix the constraints.
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(view);
[options addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[view]|" options:0 metrics:nil views:viewsDictionary]];
[options addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[view]|" options:0 metrics:nil views:viewsDictionary]];
This works to some extent: the correct content is displayed in the view and the content's width is changed according to the constraints. However, the height of the options view doesn't change, so some content of the view view is not displayed. I tried to change this by manually setting the frame, but that doesn't work. Also, I tried the following to set a constraint:
[options addConstraint:[NSLayoutConstraint constraintWithItem:options attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeHeight multiplier:1 constant:0]];
Nothing works though. Does anyone know how I can fix this?
A second problem I noticed is that even when the content of the NSScrollView is larger than the NSScrollView itself, scrolling doesn't work. No scrollbar is displayed. How can I fix that?
I still can't get my head around these harder problems with constraints...

I managed to solve my problem. It turned out I really had two problems:
I didn't set the correct constraints on the NSScrollView and its children;
I changed the content of the custom view, but didn't change its height.
My fix to both problems follows below.
NSScrollView constraints
After adding a NSScrollView in Interface Builder and placing some UI elements in the document view (the clip view's child), I had the following interface (the white part of the window is the NSScrollView, don't look at the constraints for now):
If you run this and resize the window, no scrollbars are displayed though. First of all we have to add top and leading constraints on the document view relative to its superview. Now we need to specify the size of the document view. This can be done in two ways, depending on your use-case:
Add constraints for the width and height of the document view. This way the same scrollbars are always displayed. The document view will then not change size depending on the content though, unless you automatically update these constraints;
Add constraints to every child of the document view, so that document view's width and height can be calculated. This was needed for my problem, and these constraints are displayed in the image above.
If you run this, everything works as expected. We still can't change the content of the custom view though, and that's a different problem.
Changing the custom view
Now we just need to change the content of the custom view. Since we set constraints on the custom view, these should also be changed. All of this is done with the following self-explanatory code (the custom view is named content):
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[content setAutoresizesSubviews:NO];
}
- (IBAction)button1:(id)sender {
[self showContent:view1];
}
- (IBAction)button2:(id)sender {
[self showContent:view2];
}
- (void)showContent:(NSView *)c {
// Remove all constraints.
[content removeConstraints:content.constraints];
// Add a height constraint.
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:content attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:c.frame.size.height];
[content addConstraint:constraint];
// Add a width constraint.
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:content attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:c.frame.size.width];
[content addConstraint:constraint];
[c setTranslatesAutoresizingMaskIntoConstraints:NO];
// Set the content.
content.subviews = [NSArray arrayWithObject:c];
// Add constraints from the content view to its child view.
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(c);
[content addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[c]|" options:0 metrics:nil views:viewsDictionary]];
[content addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[c]|" options:0 metrics:nil views:viewsDictionary]];
}
I hope this answer saves someone else the trouble of finding out about the strange behavior with NSScrollView and autolayout.

Related

Using Xib's for a multi-device UI

I am using a xib file to create the view for a settings view controller. The user taps an object in the main view which animates a narrow view (preferably 35% the width of the screen) over from the left.
How do I set a relative width using xibs. Right now I am setting the simulated metrics - > size property to freeform and I only have the option to hard code a width in points.
Autolayout - make a width constraint for the view:
myView.translatesAutoresizingMaskIntoConstraints = NO;
[myView addConstraint: [NSLayoutConstraint constraintWithItem:myView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:.35]];

Force multiplier to update on Interface Builder constraint

I've created a height constraint for a UIView in the Interface Builder that sets the height to = the superview. I set the multiplier to 1/3 of the superview's width. This works great, it sets the height, say on iPhone 5/5S, to ~107pts.
For wider screens, I want to set the multiplier to 1/4 instead; I suppose I could do this with Size Classes, but I'm not sure that's the best way to go for this?
In my viewWillAppear method of a view controller, I've tried doing the following; no matter what I do though, it still sets the height to only ~107pts. Note that _controlsAspectRatio is an IBOutlet to the IB constraint.
_controlsAspectRatio = [NSLayoutConstraint constraintWithItem:_controlsAspectRatio.firstItem attribute:_controlsAspectRatio.firstAttribute relatedBy:_controlsAspectRatio.relation toItem:_controlsAspectRatio.secondItem attribute:_controlsAspectRatio.secondAttribute multiplier:1.0f/4.0f constant:0];
// I've tried several combinations of several layout refresh options, to no avail
[_controlsContainer setNeedsUpdateConstraints];
[_controlsContainer setNeedsLayout];
[_controlsContainer layoutIfNeeded];
NSLog(#"%0.4f - %0.4f", _controlsContainer.frame.size.height, _controlsAspectRatio.multiplier);
// Output: 106.6667 - 0
Had to remove the original and add a completely new constraint:
NSLayoutConstraint* newControlsAspectRatio = [NSLayoutConstraint constraintWithItem:_controlsAspectRatio.firstItem attribute:_controlsAspectRatio.firstAttribute relatedBy:_controlsAspectRatio.relation toItem:_controlsAspectRatio.secondItem attribute:_controlsAspectRatio.secondAttribute multiplier:1.0f/4.0f constant:0];
[self.view removeConstraint:_controlsAspectRatio];
[self.view addConstraint:newControlsAspectRatio];
[_controlsContainer layoutIfNeeded];
NSLog(#"%0.4f - %0.4f", _controlsContainer.frame.size.height, newControlsAspectRatio.multiplier);

NSLayoutConstraint Prevents NSWindow Resizing

I am adding some simple constraints to an NSView that I have in my main NSWindow but it is causing problems.
Normally I can resize my application window (just like any Safari/Finder window etc.). I add a simple NSView to my window like so:
[self.blackDimOverlay setFrame:NSMakeRect(0, 0, self.window.frame.size.width, self.window.frame.size.height)];
[self.blackDimOverlay setAlphaValue:0.5f];
[self.window.contentView addSubview:self.blackDimOverlay];
This works as expected. I then add two constraints to this view so that it stays the full size of the window when resized.
NSDictionary *viewsDict = NSDictionaryOfVariableBindings(_blackDimOverlay);
NSArray *constraintHorizontalOverlay = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[_blackDimOverlay]-0-|" options:0 metrics:nil views:viewsDict];
NSArray *constraintVerticalOverlay = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[_blackDimOverlay]-0-|" options:0 metrics:nil views:viewsDict];
[self.window.contentView addConstraints:constraintHorizontalOverlay];
[self.window.contentView addConstraints:constraintVerticalOverlay];
However, after adding these two constraints, they lock my window up, so I can't resize it anymore. Everything else works as normal, but the constraints block any window resizing.
How can I keep my subview the full-size of my window while being able to resize my window?
Thanks.
There are additional constraints on _blackDimOverlay which dictate its size and are of higher priority than NSLayoutPriorityDragThatCanResizeWindow. You can investigate using:
NSLog(#"%#", [self.window.contentView constraintsAffectingLayoutForOrientation:NSLayoutConstraintOrientationHorizontal]);
NSLog(#"%#", [self.window.contentView constraintsAffectingLayoutForOrientation:NSLayoutConstraintOrientationVertical]);
Most likely, you forgot to do [_blackDimOverlay setTranslatesAutoresizingMaskIntoConstraints:NO].
Alternatively, the view class of which _blackDimOverlay is an instance defines an intrinsic size and its hugging and compression-resistance priorities are higher than NSLayoutPriorityDragThatCanResizeWindow. You would need to reduce those priorities using -setContentHuggingPriority:forOrientation: and -setContentCompressionResistancePriority:forOrientation:.

Autolayout of View Controller View in a Container

I have an NSWindow that has 2 container views within it, all in one xib, like so:
In another xib, I have a the view for the sidebar, managed by a different view controller, like so:
When I add the subview to the container view, I do it like this:
self.sidebarViewController.view.frame = self.sidebarContainer.bounds;
[self.sidebarContainer addSubview:self.sidebarViewController.view];
When I build and run, and resize this window, this is what happens:
The container tracks the height of it's superview properly, but the sidebar view itself does not track the height of the container.
How can I set things up so that the height of the sidebarVCs' view tracks the height of the container as the window is resized?
I think I've solved it like so:
[self.sidebarViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
NSLayoutConstraint *w = [NSLayoutConstraint constraintWithItem:self.sidebarViewController.view attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:150.0];
NSArray *c1 = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[view]|"
options:0
metrics:nil
views:#{ #"view":self.sidebarViewController.view }];
[self.sidebarContainer addConstraints:c1];
[self.sidebarContainer addConstraint:w];
I still don't yet understand why the autoresizemask layout constraint doesn't do this automatically, but I think I'm closer to understanding the relationships between the views here.
Use NSBoxes as containers. In your sidebar, add an NSBox inside the NSView and constrain all sides to the view. Then when you want to swap in a view for the side bar you can call container (the NSBox object) setContentView:.

Subview disappears with auto layout

I just added a UIView as subview to the main UIView on interface builder (basic single view application).
Without setting any constraints, my subview disappears.
subview's frame = (0 0; 320 0);
Why is that?
If I try to add some constraints like for trailing space, leading space, top space and bottom space to be fixed, still my view disappears.
How can I solve this?
Thank you.
Just to clarify thing a little I created a test project (single view application), and added 2 subview to the main view like in the image. I didn't change any default constraint.
And you can see the error in the log of the image.
Logs:
**2013-01-19 17:16:02.435 Test[8871:c07] 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)
(
"<NSAutoresizingMaskLayoutConstraint:0x106178c0 h=--- v=--- V:[UIWindow:0x9917040(480)]>",
"<NSLayoutConstraint:0x106159e0 UIView:0x991a5a0.bottom == UIWindow:0x9917040.bottom>",
"<NSLayoutConstraint:0x991ab00 V:|-(518)-[BottomView:0x9919c90] (Names: '|':UIView:0x991a5a0 )>",
"<NSLayoutConstraint:0x10615960 V:|-(20)-[UIView:0x991a5a0] (Names: '|':UIWindow:0x9917040 )>",
"<NSLayoutConstraint:0x991aa80 BottomView:0x9919c90.bottom == UIView:0x991a5a0.bottom>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x991aa80 BottomView:0x9919c90.bottom == UIView:0x991a5a0.bottom>
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.**
Constraints:
Also here is the result on the simulator:
It is good practice to understand those logs but if you are going to use Autolayout you are going to have to read up on this. Everyone says it is simply but I personally have not found it simple.
Apples Programming Guide For AutoLayout
Please read this guide especially the debugging section.
As a very very general rule if you are going to add a view then you need to turn off autoresizingmasks (Spring and struts) for the view. Add the view as a subview and give it 2 or 3 constraints. In your case above you would give it a constaint that it should have a left or leading space to superview of 0. A top space to superview of 0 and a width of 320.
EDIT; Here is an example of adding a view; note you do not need to create a frame. The constraints may be a little strange. The first puts the view in the centre of the superview. The second gives it a width of 200. The next method is the vertical constraint which puts the view at the bottom and makes it 2 high.
UIView *sView = [[UIView alloc] init];
[sView setTranslatesAutoresizingMaskIntoConstraints:NO];
[superView addSubview:sView];
[superView addConstraint:[NSLayoutConstraint constraintWithItem:sView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:superView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0]];
[superView addConstraint:[NSLayoutConstraint constraintWithItem:sView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:Nil
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:200]];
[superView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:[sView(2)]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(sView)]];