objective c - create - Reemplazando NSView mientras se mantienen las restricciones de autolayout
programmatically constraints swift 4 (4)
Quiero reemplazar un NSView
a otra vista mientras NSView
las restricciones.
Tengo una superview
, una subview
ya que es un elemento secundario y un placeholder
que planeo mover al lugar de la subvista. Pero parece que el código
[[superview] replaceSubview:subview with:placeholder];
descarta todas las restricciones relacionadas con la subview
y los resultados al eliminar la subview
.
¿Cómo se pueden "copiar" las restricciones de una vista a otra?
Básicamente terminé lo que Brad Allred sugirió, construyendo sobre su código. La siguiente categoría hace lo que hizo la pregunta original. Solo probado en un caso de uso hasta el momento :) Asume 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
Aquí hay un código que escribí hace mucho tiempo para hacer lo que me pides.
Mi código es para intercambiar dos NSViews dentro de la misma supervista, pero puedes adaptarlo fácilmente para reemplazarlo eliminando los bits innecesarios y haciendo la adición / eliminación de la vista / restricción en un orden cuidadoso. De hecho, tengo una versión más corta de este código en una clase de controlador de vista "proxy" que hace exactamente lo que usted, pero no puedo compartir porque es un proyecto propietario que no me pertenece.
Le diré que lo que necesita hacer es copiar las restricciones de la vista de proxy a la nueva vista y luego agregar la nueva vista a la supervista. Después de eso, copie las restricciones de superview para el proxy a la nueva vista y solo después de hacerlo elimine la vista de proxy de la supervista.
- (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];
}
}
puedes ignorar los bits sobre la persistencia en tu caso, imagino. En mi caso, quería implementar la funcionalidad del trampolín de iOS (poder pulsar y mantener presionado un botón, se agita, dejar que lo arrastre a otro botón y cambiar lugares mientras persista entre los lanzamientos)
En algunos casos, el método de subvista es más simple de implementar. Especialmente si tiene una vista detallada que cambia dependiendo de algunos datos.
En la ubicación donde planea mostrar las diferentes vistas de detalles, agregue una vista personalizada vacía y agregue restricciones para mantenerla en la ubicación correcta.
Crear controladores de vista para todas las vistas de detalle. Para cambiar la vista, use este código:
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)]];
}
Utiliza una sola subvista como marcador de posición que llena la vista del marcador de posición de borde a borde.
Otro enfoque es reemplazar la vista en una vista de contenedor (y no estoy hablando necesariamente de la vista de inserción de contenedor incrustado que ve en IB, pero podría ser simplemente una NSView
que contendrá la vista que se reemplaza si querer), y luego le da a ese contenedor la vista de todas las ricas restricciones que dictan la ubicación con respecto a todas las otras vistas en la supervista. De esta forma, no está lidiando con ninguna restricción complicada para la vista que se reemplaza.
A continuación, puede eliminar la antigua subvista del contenedor, agregar la nueva subvista y dar a la subvista las restricciones trivialmente simples para que aparezca en la vista del contenedor de forma adecuada:
// 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]];
Con este proceso, evita reconstruir cualquier restricción complicada que haya dictado previamente la relación de la vista reemplazada con todos sus pares anteriores en la jerarquía de vistas.