我想NSView
在保持约束的同时将一个视图替换为另一个视图。
我有一个superview
,subview
因为它是孩子placeholder
,我打算搬到 subview 的地方。但它似乎像代码
[[superview] replaceSubview:subview with:placeholder];
删除所有与相关的约束subview
并导致仅删除subview
.
如何将约束从一个视图“复制”到另一个视图?
我想NSView
在保持约束的同时将一个视图替换为另一个视图。
我有一个superview
,subview
因为它是孩子placeholder
,我打算搬到 subview 的地方。但它似乎像代码
[[superview] replaceSubview:subview with:placeholder];
删除所有与相关的约束subview
并导致仅删除subview
.
如何将约束从一个视图“复制”到另一个视图?
这是我很久以前写的一些代码,可以满足您的要求。
我的代码用于在同一个超级视图中交换两个 NSView,但您可以通过删除不需要的位并以仔细的顺序执行视图/约束添加和删除来轻松调整它以进行替换。事实上,我在“代理”视图控制器类中有一个较短版本的代码,它完全可以完成你的工作,但我不能分享它,因为它是一个不属于我的专有项目。
我会告诉你,你需要做的是将约束从代理视图复制到新视图,然后将新视图添加到超级视图。之后将代理的超级视图约束复制到新视图,并且仅在您这样做之后从超级视图中删除代理视图。
- (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 跳板功能(能够点击并按住一个按钮,它会抖动,让我将它拖到另一个按钮并交换位置,同时在启动之间坚持)
另一种方法是将要替换的视图放在容器视图中(我不一定要谈论您在 IB 中看到的嵌入 segue 容器视图,但它可能只是一个简单NSView
的包含要替换的视图的视图),然后为该容器视图提供所有丰富的约束,这些约束决定了相对于父视图上的所有其他视图的位置。这样,您就不会为被替换的视图处理任何复杂的约束。
然后您可以删除容器的旧子视图,添加新的子视图,并为该子视图提供非常简单的约束,以便它适当地出现在容器视图中:
// 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]];
通过这个过程,您可以避免重新构建任何复杂的约束,这些约束可能先前决定了被替换视图与其在视图层次结构中的所有先前对等点的关系。
在某些情况下,子视图方法更易于实现。特别是如果您有一个根据某些数据切换的详细视图。
在您计划显示不同细节视图的位置,添加一个空的自定义视图并添加约束以将其保持在正确的位置。
为所有详细视图创建视图控制器。要切换视图,请使用以下代码:
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)]];
}
它使用一个子视图作为占位符,从边到边填充占位符视图。
在他的代码的基础上,我基本上完成了 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