I want to replace one NSView
to other view while keeping the constraints.
I have a superview
, subview
as it's child and an placeholder
that I'm planning to move to subview's place.
But it seems like the code
[[superview] replaceSubview:subview with:placeholder];
drops all the constraints related to subview
and results in just removing the subview
.
How can constraints be "copied" from one view to another?
Another approach is to put the view being replaced in a container view (and I'm not necessarily talking about the embed segue container view that you see in IB, but it could just be a simple NSView
that will contain the view being replaced if you want), and then give that container view all of the rich constraints that dictate the placement with respect to all the other views on the superview. That way, you're not dealing with any complicated constraints for the view being replaced.
Then you can just remove the container's old subview, add the new subview, and give that subview the trivially simple constraints so it appears in the container view appropriately:
// remove existing subview
[[[self.containerView subviews] firstObject] removeFromSuperview];
// add new subview
NSView *subview = [self viewTwo];
[subview setTranslatesAutoresizingMaskIntoConstraints:false];
[self.containerView addSubview:subview];
// setup constraints for new subview
NSDictionary *views = NSDictionaryOfVariableBindings(subview);
[self.containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[subview]|" options:0 metrics:nil views:views]];
[self.containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[subview]|" options:0 metrics:nil views:views]];
With this process, you avoid rebuilding any complicated constraints that may have previously dictated the relationship of the replaced view with all of its former peers in the view hierarchy.
Here is some code I wrote a long time ago to do what you ask.
My code is for swapping two NSViews within the same superview, but you can easily adapt it for replacement by stripping out the unneeded bits and doing view/constraint addition and removal in a careful order. In fact I have a shorter version of this code in a "proxy" view controller class that does exactly what you, but I cannot share it because it is a proprietary project that doesn't belong to me.
I will tell you that what you need to do is copy the constraints from the proxy view to the new view then add the new view to the superview. After that copy the superview constraints for the proxy to the new view and only after you do that remove the proxy view from the superview.
- (void)swapView:(NSView*) source withView:(NSView*) dest persist:(BOOL) persist
{
NSLog(@"swapping %@ with %@", source.identifier, dest.identifier);
// !!!: adjust the "Auto Layout" constraints for the superview.
// otherwise changing the frames is impossible. (instant reversion)
// we could disable "Auto Layout", but let's try for compatibility
// TODO: we need to either enforce that the 2 controls have the same superview
// before accepting the drag operation
// or modify this code to take two diffrent superviews into account
// we are altering the constraints so iterate a copy!
NSArray* constraints = [dest.superview.constraints copy];
for (NSLayoutConstraint* constraint in constraints) {
id first = constraint.firstItem;
id second = constraint.secondItem;
id newFirst = first;
id newSecond = second;
BOOL match = NO;
if (first == dest) {
newFirst = source;
match = YES;
}
if (second == dest) {
newSecond = source;
match = YES;
}
if (first == source) {
newFirst = dest;
match = YES;
}
if (second == source) {
newSecond = dest;
match = YES;
}
if (match && newFirst) {
[dest.superview removeConstraint:constraint];
@try {
NSLayoutConstraint* newConstraint = nil;
newConstraint = [NSLayoutConstraint constraintWithItem:newFirst
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:newSecond
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant];
newConstraint.shouldBeArchived = constraint.shouldBeArchived;
newConstraint.priority = NSLayoutPriorityWindowSizeStayPut;
[dest.superview addConstraint:newConstraint];
}
@catch (NSException *exception) {
NSLog(@"Constraint exception: %@\nFor constraint: %@", exception, constraint);
}
}
}
[constraints release];
NSMutableArray* newSourceConstraints = [NSMutableArray array];
NSMutableArray* newDestConstraints = [NSMutableArray array];
// again we need a copy since we will be altering the original
constraints = [source.constraints copy];
for (NSLayoutConstraint* constraint in constraints) {
// WARNING: do not tamper with intrinsic layout constraints
if ([constraint class] == [NSLayoutConstraint class]
&& constraint.firstItem == source) {
// this is a source constraint. we need to copy it to the destination.
NSLayoutConstraint* newConstraint = nil;
newConstraint = [NSLayoutConstraint constraintWithItem:dest
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:constraint.secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant];
newConstraint.shouldBeArchived = constraint.shouldBeArchived;
[newDestConstraints addObject:newConstraint];
[source removeConstraint:constraint];
}
}
[constraints release];
// again we need a copy since we will be altering the original
constraints = [dest.constraints copy];
for (NSLayoutConstraint* constraint in constraints) {
// WARNING: do not tamper with intrinsic layout constraints
if ([constraint class] == [NSLayoutConstraint class]
&& constraint.firstItem == dest) {
// this is a destination constraint. we need to copy it to the source.
NSLayoutConstraint* newConstraint = nil;
newConstraint = [NSLayoutConstraint constraintWithItem:source
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:constraint.secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant];
newConstraint.shouldBeArchived = constraint.shouldBeArchived;
[newSourceConstraints addObject:newConstraint];
[dest removeConstraint:constraint];
}
}
[constraints release];
[dest addConstraints:newDestConstraints];
[source addConstraints:newSourceConstraints];
// auto layout makes setting the frame unnecissary, but
// we do it because its possible that a module is not using auto layout
NSRect srcRect = source.frame;
NSRect dstRect = dest.frame;
// round the coordinates!!!
// otherwise we will have problems with persistant values
srcRect.origin.x = round(srcRect.origin.x);
srcRect.origin.y = round(srcRect.origin.y);
dstRect.origin.x = round(dstRect.origin.x);
dstRect.origin.y = round(dstRect.origin.y);
source.frame = dstRect;
dest.frame = srcRect;
if (persist) {
NSString* rectString = NSStringFromRect(srcRect);
[[_theme prefrences] setObject:rectString forKey:dest.identifier];
rectString = NSStringFromRect(dstRect);
[[_theme prefrences] setObject:rectString forKey:source.identifier];
}
}
you can safely ignore the bits about persistence in your case I imagine. In my case I wanted to implement the iOS springboard functionality (being able to tap-and-hold a button, it jiggles, let me drag it to another button and swap places while persisting between launches)
In some cases the subview method is simpler to implement. Especially if you have a detail view which switches depending on some data.
At the location where you plan to show the different detail views, add a empty custom view and add constraints to keep it at the right location.
Create view controllers for all detail views. To switch the view, use this code:
id displayedObject = ...;
NSView *newDetailView = nil;
if ([displayedObject isKindOfClass:[ClassA class]]) {
_viewControllerA.representedObject = displayedObject
newDetailView = _viewControllerA.view;
} else {
_viewControllerB.representedObject = displayedObject;
newDetailView = _viewControllerB.view;
}
if (_currentDetailView != newDetailView) {
_currentDetailView = newDetailView;
for (NSView *subview in self.detailViewPlaceholder.subviews) {
[subview removeFromSuperview];
}
newDetailView.frame = self.detailViewPlaceholder.frame;
[self.detailViewPlaceholder addSubview:newDetailView];
[self.detailViewPlaceholder addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[newDetailView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(newDetailView)]];
[self.detailViewPlaceholder addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[newDetailView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(newDetailView)]];
}
It uses one single subview as placeholder which fills the placeholder view from edge to edge.
I essentially finished what Brad Allred suggested, building on his code. The following category does what the original question asked. Only tested in one use case so far :) Assumes ARC.
@interface NSView (SSYAutoLayout)
/*!
@brief Replaces a given subview of the receiver with another given view,
without changing the layout of the receiver (superview)
@details This method is handy for replacing placeholder views with real
views. It will transfer both the frame and the Auto Layout constraints, so it
works whether or not Auto Layout is in use. It is a wrapper around
-[NSView replaceSubview:with:].
@param newView The view to replace the old view. It is assumed that this
view currently has no constraints.
@param oldView The view to be replaced. All we do with this is remove
it from the superview. We do not remove any of its constraints. That should
be fine if you are going to discard this view.
*/
- (void)replaceKeepingLayoutSubview:(NSView *)oldView
with:(NSView *)newView ;
@end
@implementation NSView (SSYAutoLayout)
- (void)replaceKeepingLayoutSubview:(NSView *)oldView
with:(NSView *)newView {
/* Remember Auto Layout constraints. There are two objects which may be
"holding" relevant constraints. First, the superview of the old view may
hold constraints that refer to old view. We call these "relevant superview
constraints". Second, the old view can hold constraints upon itself.
We call these the "self constraints". The following code remembers each
in turn. */
NSMutableArray* oldRelevantSuperviewConstraints = [NSMutableArray new] ;
NSMutableArray* newRelevantSuperviewConstraints = [NSMutableArray new] ;
for (NSLayoutConstraint* constraint in self.constraints) {
BOOL isRelevant = NO ;
NSView* new1stItem ;
NSView* new2ndItem ;
if (constraint.firstItem == oldView) {
isRelevant = YES ;
new1stItem = newView ;
}
if (constraint.secondItem == oldView) {
isRelevant = YES ;
new2ndItem = newView ;
}
if (isRelevant) {
NSLayoutConstraint* newConstraint = [NSLayoutConstraint constraintWithItem:(new1stItem ? new1stItem : constraint.firstItem)
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:(new2ndItem ? new2ndItem : constraint.secondItem)
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant] ;
newConstraint.shouldBeArchived = constraint.shouldBeArchived ;
newConstraint.priority = constraint.priority ;
[oldRelevantSuperviewConstraints addObject:constraint] ;
[newRelevantSuperviewConstraints addObject:newConstraint] ;
}
}
NSMutableArray* newSelfConstraints = [NSMutableArray new] ;
for (NSLayoutConstraint* constraint in oldView.constraints) {
// WARNING: do not tamper with intrinsic layout constraints
if ([constraint class] == [NSLayoutConstraint class] && constraint.firstItem == oldView) {
NSView* new1stItem ;
NSView* new2ndItem ;
if (constraint.firstItem == oldView) {
new1stItem = newView ;
}
if (constraint.secondItem == oldView) {
new2ndItem = newView ;
}
NSLayoutConstraint* newConstraint = [NSLayoutConstraint constraintWithItem:(new1stItem ? new1stItem : constraint.firstItem)
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:(new2ndItem ? new2ndItem : constraint.secondItem)
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant] ;
newConstraint.shouldBeArchived = constraint.shouldBeArchived ;
newConstraint.priority = constraint.priority ;
[newSelfConstraints addObject:newConstraint] ;
}
}
/* Remember the old frame, in case Auto Layout is not being used. */
NSRect frame = oldView.frame ;
/* Do the replacement. */
[self replaceSubview:oldView
with:newView] ;
/* Replace frame and constraints. */
newView.frame = frame ;
[newView addConstraints:newSelfConstraints] ;
[self removeConstraints:oldRelevantSuperviewConstraints] ;
[self addConstraints:newRelevantSuperviewConstraints] ;
}
@end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With