Adding NSLayoutConstraint from code - xcode

I am having a lot of problems understanding constraints made in code. I have this container view that is created and set in IB and then in that container NSView's initWithFrame I add the child NSView like this (self is the container view):
childView = [[NSView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, tabContainerHeight + tabContainerTopSpace)];
[childView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:childView];
NSLayoutConstraint *tabContainerConstraint = [NSLayoutConstraint constraintWithItem:childView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
[childView addConstraint:tabContainerConstraint];
tabContainerConstraint = [NSLayoutConstraint constraintWithItem:childView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
[childView addConstraint:tabContainerConstraint];
tabContainerConstraint = [NSLayoutConstraint constraintWithItem:childView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
[childView addConstraint:tabContainerConstraint];
The problem right now is that the child view is not visible at all, I don't know what happens to it. What I want to do is to have the child view to always have its top exactly at the container view's top and the same goes for left and right, so the child view must have a fixed height but always placed at the top of the container view and then stretch to the sides with the container view (like in the attached image if it explains it).
How is this done from code?
Thank you
Søren

There are a few problems here.
First, you're using initWithFrame: and then turning on autolayout - this means your frame information is going to get discarded. You need a height constraint for your child view.
Second, you're adding the constraints to the child view instead of its superview. The autolayout system may be able to figure this out but the constraints should really be added to self in this case.
You can check the frames of your views in the debugger or an introspection tool like Reveal to see where things might be going wrong - I'd guess in this case you have a height of zero in your child view.
Thirdly (this one isn't necessarily a problem), the code you're using is unnecessarily verbose. The individual creation format is good to help understand the constraints you are making and good for complex cross-hierarchy constraints but it can leave you with ambiguous layouts or adding constraints to the wrong views. The layout you are after is well suited to using the visual format language - it would only be two statements:
For your horizontal layout:
#"|[childView]|"
For your vertical layout:
#"V:|[childView(==height)]"
Where height is either a key in your metrics dictionary, or you replace it with the hard coded height number.
I've written in tedious detail about the various ways to create constraints in code: visual format language and individual constraints. These are iOS focused but autolayout is very similar on both frameworks.

Related

how to make expanding view with autolayout?

(I don't know english well). I have a question: can you show an example of my problem. I want to make expanding list. i.e. i have a controller view and when I click on one of strings there appears a list with data of this strong under it, and other string shift below.
You can achieve this with an additional(!) constraint for the height, by which you can set the view's height to zero:
Define this constraint in the storyboard (or code).
Create an outlet property to the constraint. This must be a strong property because of deactivating a constraint will remove it from the corresponding views.
Give this constraint the highest priority. Other constraints which influence the hight should have smaller priorities.
You can now hide and show the view by activating and deactivating the constraint.
Animation of the change can be done with UIKit animations:
UIView *theView = self.view; // Must be a parent of the animated view
self.heightConstraint.active = isHidden;
[UIView animateWithDuration:0.25 animations:^{
[theView layoutIfNeeded];
}

Animation with Autolayout

I don't use IB. I am laying everything out with code.
I have a UIToolbar on top of a view.
I am setting its height to 64 or something and the width obviously to stretch horizontally to fit screen width.
When I first launch the app, everything is perfect. I change the orientation, toolbar resizes, all good.
Now comes the animation part.
When I swipe up on the view, the toolbar should move up on top of the screen and hide.
When I swipe down on the view, the toolbar should come back down.
I tried setting the constraints in the following manner.
Initially this.
// for width
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[toolbar]-|" options:0 metrics:nil views:viewDict]];
// for height
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[toolbar(toolbarHeight)]" options:0 metrics:metricDict views:viewDict]];
Now, during Swipe Up
[self.view removeConstraint:[NSLayoutConstraint constraintWithItem:self.toolBar attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.toolBar attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0]];
But it is breaking the constraint that is supposed to bring it back down and staying up.
Needless to say, it is driving me mad. Any solutions?
EDIT:
On closer analysis, the "removeConstraint" wasn't working as expected. Basically, it wasn't removing the constraint I wanted it to remove. self.view.constraints is an NSArray. Now I need to find a simple way to remove the exact constraint I want.
So you just need to create your heigh constraint using the long form for creating a single constraint. And then assign it to a property.
Then all you have to do is change the constant value.
So swipe up would set the height constraint constant to zero. Swipe down would change the height constraint constant to desired height.
You don't need to add and remove that constraint every time.

How to chain NSView to simulate pagination?

I'd like to chain several views together and have content flow from one view to the next automatically. Think of how text containers work and how their content can span across containers. Does anyone have an idea how this might be done?
You could build something like this from scratch, with a layout manager which manages a set of container views.
This code is designed to vertically resize a container to hold its subviews:
+ (void)setAndArrangeSubviews:(NSArray *)subviews inView:(NSView *)superview {
[superview setSubviews:subviews];
NSRect superviewFrame = [superview frame];
CGFloat y = superviewFrame.size.height;
for (NSView *subview in subviews) {
NSRect subviewFrame = [subview frame];
subviewFrame.origin.y = (y -= subviewFrame.size.height);
[subview setFrame:subviewFrame];
}
}
You could adapt it to accomplish what you want: arranging subviews in the container until it's full, then arranging the remaining views in the next container.
If you only need to stack the views vertically, this seems an easy enough way to accomplish what you want.
The answer to this related question refers to a 10.7 feature called Cocoa Auto Layout, which may provide a more automatic way to accomplish this, which might be worth investigating if you need to lay these out in 2D.

NSSplitView and autolayout

How should I use auto layout constrains inside NSSplitView subview?
My NSSplitView subview has 3 subview: topPane, tableContainer and bottomPane and I set the constrains like this:
NSDictionary* views = NSDictionaryOfVariableBindings(topPane, tableContainer, bottomPane);
for (NSView* view in [views allValues]) {
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
}
[myView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[topPane(34)][tableContainer][bottomPane(24)]|"
options:0
metrics:nil
views:views]];
[mySplitView addSubview:myView];
And got this in console:
Unable to simultaneously satisfy constraints:
(
"<NSLayoutConstraint:0x7fd6c4b1f770 V:[NSScrollView:0x7fd6c4b234c0]-(0)-[CPane:0x7fd6c4b2fd10]>",
"<NSLayoutConstraint:0x7fd6c4b30910 V:[CPane:0x7fd6c4b2f870(34)]>",
"<NSLayoutConstraint:0x7fd6c4b30770 V:|-(0)-[CPane:0x7fd6c4b2f870] (Names: '|':NSView:0x7fd6c4b22e50 )>",
"<NSLayoutConstraint:0x7fd6c4b212f0 V:[CPane:0x7fd6c4b2fd10]-(0)-| (Names: '|':NSView:0x7fd6c4b22e50 )>",
"<NSLayoutConstraint:0x7fd6c4b2f910 V:[CPane:0x7fd6c4b2f870]-(0)-[NSScrollView:0x7fd6c4b234c0]>",
"<NSLayoutConstraint:0x7fd6c4b21290 V:[CPane:0x7fd6c4b2fd10(24)]>",
"<NSAutoresizingMaskLayoutConstraint:0x7fd6c3630430 h=--& v=--& V:[NSView:0x7fd6c4b22e50(0)]>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fd6c4b1f770 V:[NSScrollView:0x7fd6c4b234c0]-(0)-[CPane:0x7fd6c4b2fd10]>
I think <NSAutoresizingMaskLayoutConstraint:0x7fd6c3630430 h=--& v=--& V:[NSView:0x7fd6c4b22e50(0)]> causes this, but I can't reset autoresizing mask, because NSSplitView sets it.
What is best way to use auto layout inside split view? And is there any way to handle min/max size of split view subview with auto layout without NSSplitViewDelegate?
I found out that this error appears if I have toolbar in my window and control split view by any of this delegate methods:
splitView:constrainMinCoordinate:ofSubviewAt:
splitView:constrainMaxCoordinate:ofSubviewAt:
splitView:shouldAdjustSizeOfSubview:
Solution was found in attaching toolbar to window in windowDidLoad.
NSSplitView has been a strange thing since the beginning and it would not surprise me if it'll be gone soon. After trying to get NSSplitView working with AutoLayout for a month now and sinking from one despair attack to another, I finally gave up.
My solution is to not use NSSplitView with AutoLayout at all. So either NSSplitView without Autolayout or Autolayout without NSSplitView: this isn't as complicated as it sounds: just lay out your subviews next to each other and add NSLayoutConstraints as IBOutlets. The constants of these constraints can then be set and changed from the controller in code. With that approach you can set the origin (negative offset to slide it out of the window), the width and the relations to other subviews - plus it's really easy to animate constraints with the view's animator (ever tried to animate a NSSplitView?)
The only thing missing is the mouse drag on the dividers, but this can be implemented with a couple of lines, tracking mouseEvents in your custom "SplitView".
There's an autolayout "splitview" example from Apple (unfortunately only vertical) and I've seen at least one new project on github lately. Though for me, I thought it'd be easier to start over with my custom solution for my app's specific needs, rather than trying to create something very universal (thus making it too complex to handle).
Edit: I now completed my custom splitView that loads its subviews from separate nibs. No constraint issues, no autolayout warnings. Compared to the whole month of trying to get it work with NSSplitView, I have now a working custom splitView based on constraints, easily animatable, created in only one evening. I definitely recommend taking this route!
For anyone who stumbles onto this in the future and is looking for a jump-start into constraint-based NSSplitView replacements, I wrote a small project here that attempts to recreate a portion of NSSplitView's features using Auto Layout:
https://github.com/jwilling/JWSplitView
It's somewhat buggy, but it could be a useful reference to anyone wanting to go this route.
10.8 fixed that problem, see its release notes.
Here is my solution for 10.7 (a custom split view):
https://github.com/benuri/HASplitView.git
You do not want to disable translatesAutoresizingMaskIntoConstraints at all. You shouldn't mess with system views constraints. NSSplitView handles the sizing for the individual views itself and you are essentially trying to rip it's control away. Not to mention, you forgot to account for the splitter.
The correct way to set a minimum or maximum (or constant for that matter) width/height on a splitview is to set those things on the views individually. In particular, if you are doing this in code you will need to use 2 separate calls to constraintsWithVisualFormat, because otherwise the visual format language will create constraints between the views.
You can do all of this in IB just fine. You can even set the priority of each view in the split view, which will cause one or the other view to resize when the window does rather than distributing the resize equally.
As much as I hate to disagree, but Auco's answer should't be voted highest. It is not in any way helpful in solving the problem with an adequate amount of work. In my opinion the NSSplitView was only ever a problem to those who didn't read the documentation well enough.
The actual solution to the problem mentioned here is fairly simple: Auto Layout introduced the new "Holding Priorities API" on NSSplitView. And as the documentation says: Setting lower values to the holding priority of a subview will make him more likely to take width earlier. All of this can be set in IB and programmatically without any despair. The amount of work needed: 20 seconds approx.
I load all by a nib file and setTranslatesAutoresizingMaskIntoConstraints:NO afterwards.
So maybe you should first add by [mySplitView addSubview:myView]; your views and disable afterwards the translation of the autosizing mask to constraints and after this you add your contraint to myView.
EDIT:
Ok it seems I missunderstand the myView.
You have to add the constraint to the subviews and not to the splitview.
[topPane addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[topPane(34)]" options:0 metrics:nil views:views]];
[bottomPane addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[bottomPane(24)]" options:0 metrics:nil views:views]];
You don't have to add edge constraints (the "|" in "V:|[topPane(34)]") because the subviews in NSSplitView are already autoresizing.
This lead to this e.g. for the topPane constraint:
NOTE: ignore the subview content, they are just placeholders
It took me some time to get my autolayout clean of warnings but I did get it handled in IB (several splitviews and subviews).
My layout looks like:
RootView
|--1st NSSplitView (3 vertical subviews)
|----UIView (left)
|----2nd NSSplitView (center & 2 horizontal subviews)
|---UIView (top)
|---3rd NSSplitView (bottom & 3 vertical subviews)
|---UIView (left)
|---UIView (center)
|---UIView (right)
|----UIView (right)
My problem was, that I had 19 Warnings in all my subviews but my layout looked fine and worked how it should be.
After a while I found the cause of my warnings: the constraints of the outer views in my first splitview.
Both views (left and right) had a width-constraint with "width >= 200" and the center view (2nd splitview) had no constraints (because its min-width and max-width where handled by its subviews).
The warnings showed me that autolayout wants to shrink my IB-UI-Layout because the calculated min-widths where smaller than my layout but I didn´t want to shrink it in IB.
I added a fixed constraint "width = 200" to both of the outer subviews of my first splitview and checked "remove at build time".
Now my layout is free of warnings and everything works how it should be.
My conclusion:
I think the problem with autolayout and splitviews is that autolayout can not handle the width-constraints of the subviews. The reason we want to use splitviews is, that we want dynamic width of the views and we want it in both directions, shrink and expans.
So there is no width <= xxx && width >= xxx . Autolayout can only handle one of it and we get warnings in IB. You can fix this problem with a temporary constraint in IB which will removed before runtime.
I hope it makes sense what I wrote but it worked fine in my project.
PS: I could not found any solution until today where I found this thread.. so I guess your posts inspired me :-)
I used this class as a workaround, it's not perfect (the subviews stutter a bit) but it unblocked me. I use this class as the custom class inside each split view pane.
#interface FBSplitPaneView : NSView
#end
#implementation FBSplitPaneView
- (void)setFrame:(NSRect)frame
{
for (NSView *subview in self.subviews) {
subview.frame = self.bounds;
}
[super setFrame:frame];
}
#end

How to disable resizing of the subview of NSSplitView in Interface Builder?

I've created in Interface Builder a NSSplitView with two subviews. I want the left-side view to have fixed width. I've tried to define autosizing rules for both subviews but the left subview still changes width on window resizing (split view fills up a window). May be that caused by NSSplitView's Autoresizes Subviews property? (I can't uncheck it). What can I do?
The best way I found to do this in Interface Builder:
Drop the NSSplitView on the window
Select the Custom View you want fixed
Go up to the Xcode menu and select Editor > Pin > Width
Adjust the Constant in the Attributes Inspector to the size that you want the panel to be fixed at
Of course, you can also add this layout constraint through code as suggested above if you're feeling adventurous.
The behavior that you want required some code that you can do on the NSSplitView's delegate. However, you can have the same result using BWToolKit.
I think it should work with a NSLayoutConstraint, I work at the moment on at :).
EDIT:
Maybe to provide more details on my answer based on the comment hayden. You can define a constraint either by code or in the the IB.
In the IB select your left subview and click on the constraint buttons in the lower right corner defining a width constraints. If you select this new constraint now you can setup the the width an say it should be equal and set the size you like.
The seconed way is to create in code a NSLayoutConstraint object, i do it like this (this is just an example, and define not a fix width).
// define for the view: Constraint and AutoresizingMask option
NSView *view = self.view;
[view setTranslatesAutoresizingMaskIntoConstraints:NO]; // disable AutoresizingMask
NSDictionary *views = NSDictionaryOfVariableBindings(view);
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"[view(>=140,<=220)]" options:0 metrics:nil views:views]];
In general you find documentation to this topic under the term Auto Layout. To use it you have to enable auto layout and this featuer replace the old autosizing functions. (therefore i disable autosizing mask in the code).
This feature is quit new and you can do complex stuff with it but i think I is worth to study.

Resources