Remplacer NSView tout en gardant les contraintes d'autolayout

je veux remplacer un NSView autre point de vue, tout en conservant les contraintes.

j'ai un superview, subview comme il est enfant et un placeholder que j'ai l'intention de déménager chez subview. Mais il semble que le code

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

supprime toutes les contraintes liées à l' subview et entraîne la suppression de subview.

comment les contraintes peuvent-elles être "copiées" d'une vue à l'autre?

10
demandé sur ULazdins 2013-11-06 19:56:51

4 réponses

Voici un code que j'ai écrit il y a longtemps à faire ce que vous demandez.

Mon code est pour la permutation de deux NSViews au sein de la même superview, mais vous pouvez facilement l'adapter pour le remplacement en enlevant l'inutile bits et faire afficher/contrainte ajout et la suppression dans une attention à l'ordre. En fait, j'ai une version plus courte de ce code dans une classe de controller de vue "proxy" qui fait exactement ce que vous faites, mais je ne peux pas la partager parce que c'est un projet propriétaire qui n'appartient pas à je.

je vais vous dire que ce que vous devez faire est de copier les contraintes de la vue proxy vers la nouvelle vue puis d'ajouter la nouvelle vue à la superview. Copiez le superview contraintes pour le serveur proxy pour la nouvelle vue et seulement après que supprimer le proxy vue de la 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];
    }
}

Vous pouvez sans risque ignorer les bits sur la persistance dans votre cas j'imagine. Dans mon cas, je voulais mettre en œuvre la fonctionnalité iOS springboard (être en mesure de tap-and-hold un bouton, il se dandine, permettez-moi de glisser vers un autre bouton et lieux d'échange tout en persistant entre les lancements)

11
répondu Brad Allred 2013-11-07 15:45:42

dans certains cas, la méthode subview est plus simple à mettre en oeuvre. Surtout si vous avez une vue de détail qui change en fonction de certaines données.

à l'endroit où vous prévoyez de montrer les différentes vues de détail, ajouter une vue personnalisée vide et ajouter des contraintes pour le garder au bon endroit.

créer des contrôleurs de vue pour toutes les vues de détail. Pour changer la vue, utilisez ce 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)]];
}

il utilise un seul sous-vue comme paramètre qui remplit le vue de l'espace réservé d'un bord à l'autre.

2
répondu Flovdis 2014-03-10 17:11:10

une autre approche consiste à mettre la vue étant remplacée dans une vue de conteneur (et je ne parle pas nécessairement de la vue de conteneur embed segue que vous voyez dans IB, mais il pourrait juste être un simple NSView qui contiendra la vue étant remplacée si vous voulez), et puis donner cette vue de conteneur toutes les contraintes riches qui dictent le placement par rapport à toutes les autres vues sur la superview. De cette façon, vous n'êtes pas confrontés à des contraintes compliquées pour la vue étant remplacer.

alors vous pouvez simplement supprimer l'ancienne sous-vue du conteneur, ajouter la nouvelle sous-vue, et donner à cette sous-vue les contraintes trivialement simples afin qu'elle apparaisse dans la vue du conteneur de manière appropriée:

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

avec ce processus, vous évitez de reconstruire toute contrainte compliquée qui pourrait avoir dicté auparavant la relation de la vue remplacée avec tous ses anciens pairs dans la hiérarchie de la vue.

2
répondu Rob 2015-11-17 23:07:17

j'ai essentiellement terminé ce que Brad Allred a suggéré, en se basant sur son code.  La catégorie suivante correspond à la question initiale.  Seulement testé dans un cas d'utilisation jusqu'à présent :) suppose 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
1
répondu Jerry Krinock 2015-11-17 17:26:29