Reputation: 159
So I have two rectangular views and a view that connects them together. See diagram http://cl.ly/image/340Q1l381b3L
Is it possible to constrain the red view such that it stays within the green views vertical bounds? I'd like to keep it centered in their overlapping region.
Although this wouldn't keep it centered necessarily, I thought I'd just be able to apply the following two constraints to both green views and the red view to at least keep the red view bound to the green views.
[NSLayoutConstraint
constraintWithItem:redView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:greenView
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:0.0f];
[NSLayoutConstraint
constraintWithItem:redView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:greenView
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.0f];
But this results in
2013-06-26 22:13:27.493 MiniMeasure[25896:303] Unable to simultaneously satisfy constraints: ( "NSLayoutConstraint:0x1002cebf0 RedView:0x1002cdf90.centerY >= GreenView:0x1018a34c0.bottom", "NSLayoutConstraint:0x1002cf720 RedView:0x1002cdf90.centerY <= GreenView:0x1018a34c0.top", "NSLayoutConstraint:0x1002bb1e0 V:[GreenView:0x1018a34c0(157)]" ) Will attempt to recover by breaking constraint NSLayoutConstraint:0x1002bb1e0 V:[GreenView:0x1018a34c0(157)]
So clearly it doesn't like that one of the green views has a height constraint and tries to break it. But I need the green views to maintain their sizes as well. Both green views have width and height constraints.
Any thoughts/suggestions?
Thanks!
Upvotes: 2
Views: 2725
Reputation: 385998
You can get what you want with autolayout. Demonstration:
However, if the green views don't have any vertical overlap, the results might not be what you want:
So, how did I do it?
First of all, there are five views. There's the window's content view, two draggable green views, the text field (“Center”), and the tan spacer view. The draggable views, the text field, and the spacer are all direct subviews of the window's content view. In particular, the text field is not a subview of the spacer view.
Second, I'll need to set the priority of some constraints, so I define a helper function:
static NSLayoutConstraint *constraintWithPriority(NSLayoutConstraint *constraint,
NSLayoutPriority priority)
{
constraint.priority = priority;
return constraint;
}
Next I create the left draggable view. I set up constraints for its X position and width, its height, and its Y position.
- (void)createLeftView {
leftView = [[DraggableView alloc] init];
leftView.translatesAutoresizingMaskIntoConstraints = NO;
[rootView addSubview:leftView];
NSDictionary *views = NSDictionaryOfVariableBindings(leftView);
[rootView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"H:|-(20)-[leftView(80)]"
options:0 metrics:nil views:views]];
[leftView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[leftView(120)]"
options:0 metrics:nil views:views]];
leftView.yConstraint = [NSLayoutConstraint
constraintWithItem:leftView attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:rootView attribute:NSLayoutAttributeTop
multiplier:1 constant:20];
[rootView addConstraint:leftView.yConstraint];
}
Note that my DraggableView
class handles mouse drags by adjusting the constant of its yConstraint
. Since I need access to the Top constraint, I set that one up directly instead of using the visual format.
I create the right draggable view very similarly, except that it's anchored to the trailing edge of the root view.
- (void)createRightView {
rightView = [[DraggableView alloc] init];
rightView.translatesAutoresizingMaskIntoConstraints = NO;
[rootView addSubview:rightView];
NSDictionary *views = NSDictionaryOfVariableBindings(rightView);
[rootView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"H:[rightView(80)]-(20)-|"
options:0 metrics:nil views:views]];
[rightView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[rightView(120)]"
options:0 metrics:nil views:views]];
rightView.yConstraint = [NSLayoutConstraint
constraintWithItem:rightView attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:rootView attribute:NSLayoutAttributeTop
multiplier:1 constant:20];
[rootView addConstraint:rightView.yConstraint];
}
Now comes the tricky part. I create an extra view which I call the spacer. I've left the view visible to make it easier to understand how this demo works. Normally you'd make the spacer hidden; hidden views still participate in layout.
- (void)createSpacerView {
spacerView = [[SpacerView alloc] init];
spacerView.translatesAutoresizingMaskIntoConstraints = NO;
[rootView addSubview:spacerView];
The spacer's horizontal constraints are simple. The spacer's leading edge is pinned to the left draggable view's trailing edge, and the spacer's trailing edge is pinned to the right draggable view's leading edge. Thus the spacer always exactly spans the horizontal gap between the draggable (green) views.
NSDictionary *views = NSDictionaryOfVariableBindings(leftView, rightView, spacerView);
[rootView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"H:[leftView][spacerView][rightView]"
options:0 metrics:nil views:views]];
Next, we constrain the spacer's top and bottom edge to equal the root view's top and bottom edges, but with priority 1 (extremely low priority):
[rootView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"V:|-(0@1)-[spacerView]-(0@1)-|"
options:0 metrics:nil views:views]];
Then we further constrain the spacer's top edge to be greater than or equal to the top edges of both the draggable views' top edges, with priority 2 (almost as extremely low):
[rootView addConstraint:constraintWithPriority([NSLayoutConstraint
constraintWithItem:spacerView attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:leftView attribute:NSLayoutAttributeTop
multiplier:1 constant:0], 2)];
[rootView addConstraint:constraintWithPriority([NSLayoutConstraint
constraintWithItem:spacerView attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:rightView attribute:NSLayoutAttributeTop multiplier:1 constant:0], 2)];
So the spacer's top edge has three constraints:
These constraints combine to put the spacer's top edge as high as possible while not being above both draggable views' top edges. Thus the spacer top edge will be equal to the lower of the top edges of the draggable views.
We need to use low priorities here because auto layout will refuse to make the spacer's height negative. If we used priority 1000 (the default), auto layout would start resizing the draggable views to force them to always have some vertical overlap.
We set up similar constraints on the spacer's bottom edge:
[rootView addConstraint:constraintWithPriority([NSLayoutConstraint
constraintWithItem:spacerView attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:leftView attribute:NSLayoutAttributeBottom
multiplier:1 constant:0], 2)];
[rootView addConstraint:constraintWithPriority([NSLayoutConstraint
constraintWithItem:spacerView attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:rightView attribute:NSLayoutAttributeBottom
multiplier:1 constant:0], 2)];
}
Thus the spacer will always span the vertical overlap of the draggable views, if there is overlap. If there is no overlap, auto layout will break some constraint to avoid giving the spacer a negative height. The spacer will end up with height 0, pinned to one of the near edges of the draggable views.
Finally, I set up the text field to stay centered horizontally and vertically over the spacer:
- (void)createMiddleView {
middleView = [[NSTextField alloc] init];
middleView.translatesAutoresizingMaskIntoConstraints = NO;
middleView.stringValue = @"Center";
[middleView setEditable:NO];
[middleView setSelectable:NO];
[rootView addSubview:middleView];
[rootView addConstraint:[NSLayoutConstraint
constraintWithItem:middleView attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:spacerView attribute:NSLayoutAttributeCenterX
multiplier:1 constant:0]];
[rootView addConstraint:[NSLayoutConstraint
constraintWithItem:middleView attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:spacerView attribute:NSLayoutAttributeCenterY
multiplier:1 constant:0]];
}
With all of these constraints between the five views (including the root view), auto layout keeps the middle view centered in the way you requested.
I'm not sure if it's possible to set up constraints like this in a nib as of Xcode 4.6.3. If it's possible, I'm sure it's very painful.
You can find my full demo project source code in this github repository. I created the project in Xcode 5-DP2.
Upvotes: 12
Reputation: 12782
Based on the comments, one solution is to not use auto layout for the red rect, but to simply use drawRect in the super view or in a subview that is the same frame rect as the super view.
First use KVO to be notified of changes to the green rects frames our bounds (as appropriate for your implementation )
Changed?
If you use NSUnionRect on the two green rects a few if statements can tell you whether to display the red rect, and provide the logic for where to draw or position the rect.
If the union rect is greater than the sum of the green rects in one direction and smaller in the other direction, draw a red rect.
Where to draw can by comparing their edges.
Pseudo code If the unionRect.size.x > (greenRect1.size.x + greenRect2.size.x) AND unionRect.size.y < (greenRect1.size.y + greenRect2.size.y) Then draw a red rect between them horizontally. Now calculate the rect for red rect.
You could use this same approach figure out constraints to use for red rect relative to green rects but that will actually add complexity.
I'm not convinced that everything belongs in auto layout for the amount of work and complexity and potential obfuscation of meaning.
Upvotes: 0