Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replacing NSView while keeping autolayout constraints

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?

like image 708
ULazdins Avatar asked Nov 06 '13 15:11

ULazdins


4 Answers

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.

like image 114
Rob Avatar answered Nov 15 '22 17:11

Rob


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)

like image 23
Brad Allred Avatar answered Nov 15 '22 19:11

Brad Allred


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.

like image 40
Flovdis Avatar answered Nov 15 '22 17:11

Flovdis


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
like image 32
Jerry Krinock Avatar answered Nov 15 '22 18:11

Jerry Krinock