13

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?

ULazdins
  • 1,975
  • 4
  • 25
  • 31
  • (not that I think this will work) but have you tried newView.constraints = [oldView.constraints copy];`? I think it's less error-prone to leave the constraint system alone - it's very prickly at the best of times... – trojanfoe Nov 06 '13 at 16:00
  • can't do it. So why replace it? Why not just add placeholder as a subview with some new constraints that flush placeholder to subview, e.g. @"|-(0)-[placeholder]-(0)-|" and @"V:|-(0)-[placeholder]-(0)-|" – Max MacLeod Nov 06 '13 at 16:02
  • @trojanfoe Tried just now, didn't work. – ULazdins Nov 06 '13 at 16:05
  • @MaxMacLeod Pretty good idea if you're correct that it can't be done – ULazdins Nov 06 '13 at 16:06
  • 1
    @ULazdins I mean it can't be done other than recreate them and re-add. I either have the placeholder passed in as a parameter on my view's factory method, e.g. +(MyCustomView)myCustomViewWithAutoLayoutPlaceholder: etc. Or, I just use a UIView category method that creates the flush constraints to the placeholder – Max MacLeod Nov 06 '13 at 16:18

4 Answers4

12

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)

Brad Allred
  • 7,323
  • 1
  • 30
  • 49
  • Pretty good solution! If I comment out last `if` block (`if (persist) { ... }`), it really does the job! – ULazdins Nov 07 '13 at 07:43
  • be aware that the code above only works for views with the same superview. you need to modify it as per my instructions if you wish to use an arbitrary view. If this works for you please mark the it accepted. – Brad Allred Nov 07 '13 at 15:44
  • @ULazdins you can actually comment out/delete everything after `[source addConstraints:newSourceConstraints];` – Brad Allred Nov 07 '13 at 15:59
  • I was waiting a bit for someone else to take a part in our discussion. Seems like your solution is the only and the best. Accepted! – ULazdins Nov 08 '13 at 15:00
  • You shouldn't be calling `[dest removeConstraint:constraint];` from inside the for-loop, that's not a safe operation. Use `removeConstraints:` after the for-loop instead, this also removes the need for the `copy`. – Awesome-o Feb 12 '15 at 01:09
  • You're also not transferring all the `priority` attributes as far as I can tell. – Awesome-o Feb 12 '15 at 01:09
  • `[constraint class] == [NSLayoutConstraint class]` should be `[constraint isKindOfClass:[NSLayoutConstraint class]]` but more importantly why is this call even needed? – Awesome-o Feb 12 '15 at 01:43
  • @Awesome-o I wrote this code long ago so its possible some of what you say is true, but the class comparison you suggested wont work as per the comment that says we mustn't tamper with the intrinsic layout constraints. your suggested check would break that, which leads me to believe you are making suggestions without even testing them calling the other comments you made into question. – Brad Allred Feb 12 '15 at 05:20
  • @Awesome-o with that said, if you *did* test your modifications then I will happily update my answer. – Brad Allred Feb 12 '15 at 05:24
  • Maybe I don't understand the context then, what are intrinsic layout constraints and what makes them different than any other constraint? I assumed they were constraints on height/width, which in my case you'd want to include. – Awesome-o Feb 13 '15 at 21:27
4

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
2

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.

Flovdis
  • 2,945
  • 26
  • 49
1

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
Jerry Krinock
  • 4,860
  • 33
  • 39