在保持自动布局约束的同时替换NSView

时间:2013-11-06 15:56:51

标签: objective-c autolayout nsview

我想在保留约束的同时将一个NSView替换为其他视图。

我有一个superviewsubview因为它是孩子,而placeholder我打算转移到子视图的位置。 但它似乎是代码

[[superview] replaceSubview:subview with:placeholder];

删除与subview相关的所有约束,只会移除subview

如何将约束从一个视图“复制”到另一个视图?

4 个答案:

答案 0 :(得分:12)

这是我很久以前写的一些代码,用来做你所要求的。

我的代码用于在同一个superview中交换两个NSView,但是您可以通过删除不需要的位并按照谨慎的顺序添加和删除视图/约束来轻松地对其进行修改以进行替换。事实上,我在“代理”视图控制器类中有一个较短版本的代码,它完全符合您的要求,但我不能共享它,因为它是一个不属于我的专有项目。

我将告诉您,您需要做的是将约束从代理视图复制到新视图,然后将新视图添加到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];
    }
}

你可以放心地忽略你想象中的持久性。在我的情况下,我想实现iOS跳板功能(能够点按并按住按钮,它会摇晃,让我将其拖动到另一个按钮并交换位置,同时在启动之间保持不变)

答案 1 :(得分:2)

在某些情况下,子视图方法更容易实现。特别是如果您有一个根据某些数据切换的详细信息视图。

在您计划显示不同详细信息视图的位置,添加一个空的自定义视图并添加约束以将其保留在正确的位置。

为所有详细信息视图创建视图控制器。要切换视图,请使用以下代码:

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)]];
}

它使用一个单独的子视图作为占位符,从边到边填充占位符视图。

答案 2 :(得分:2)

另一种方法是将视图替换为容器视图(我不一定要谈论你在IB中看到的嵌入segue容器视图,但它可能只是一个简单的NSView包含要替换的视图(如果需要),然后为该容器查看所有丰富的约束,这些约束指示相对于superview上所有其他视图的放置。这样,您就不会处理被替换视图的任何复杂约束。

然后你可以删除容器的旧子视图,添加新的子视图,并为该子视图提供简单的简单约束,使其适当地出现在容器视图中:

// 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]];

通过此过程,您可以避免重建任何先前可能已将替换视图与视图层次结构中所有以前的对等项关系决定的复杂约束。

答案 3 :(得分:1)

我基本完成了Brad Allred建议的内容,并以他的代码为基础。以下类别执行原始问题的要求。到目前为止仅在一个用例中测试过:)假设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