2013-11-06 5 views
10

Я хочу заменить один NSView на другое представление, сохраняя ограничения.Замена NSView при сохранении ограничений автоопределения

У меня есть superview, subview как его ребенок и placeholder, что я планирую перейти на место подсмотра. Но, похоже, код

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

падает все ограничения, связанные с subview и приводит только удалением subview.

Как можно «скопировать» ограничения с одного вида на другой?

+0

(не то, что я думаю, что это сработает), но вы пробовали newView.constraints = [oldView.constraints copy]; '? Я думаю, что он менее подвержен ошибкам, чтобы оставить систему ограничений в одиночку - это очень колючий в лучшие времена ... – trojanfoe

+0

не может этого сделать. Так зачем его заменять? Почему бы просто не добавить placeholder в качестве подзаголовка с некоторыми новыми ограничениями, которые скрывают заполнитель для подсмотра, например. @ "| - (0) - [заполнитель] - (0) - |" и @ "V: | - (0) - [placeholder] - (0) - |" –

+0

@trojanfoe Пробовал только сейчас, не работал. – ULazdins

ответ

10

Вот код, который я написал давно, чтобы сделать то, что вы просите.

Мой код предназначен для замены двух NSViews в пределах одного супервизора, но вы можете легко адаптировать его для замены, удалив ненужные биты и сделав дополнение и удаление вида/ограничения в тщательном порядке. На самом деле у меня есть более короткая версия этого кода в классе контроллера «прокси», который делает именно то, что вы, но я не могу поделиться им, потому что это собственный проект, который не принадлежит мне.

Сообщаю вам, что вам нужно скопировать ограничения с прокси-представления на новый вид, а затем добавить новый вид в супервизор. После этого скопируйте ограничения супервизора для прокси на новое представление и только после этого удалите прокси-представление из супервизора.

- (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 (возможность нажатия и удержания кнопки, смещение, возможность перетащить его на другую кнопку и поменять местами, сохраняя при этом между запусками)

+0

Довольно хорошее решение! Если я прокомментирую последний блок 'if' (' if (persist) {...} '), он действительно выполняет эту работу! – ULazdins

+0

помните, что приведенный выше код работает только для представлений с тем же супервидом. вам необходимо изменить его в соответствии с моими инструкциями, если вы хотите использовать произвольное представление. Если это работает для вас, отметьте его принятым. –

+0

@ULazdins вы можете фактически комментировать/удалять все после '[source addConstraints: newSourceConstraints];' –

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

Он использует один единственный подвид, как заполнитель, который заполняет вид шаблонный от края до края.

0

Я по существу закончил то, что предложил Брэд Оллред, опираясь на его код. Следующая категория делает то, что задал первоначальный вопрос. До сих пор тестировался только в одном варианте использования. Предполагается 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 
2

Другой подход заключается поставить вид заменяется на вид контейнера (и я не обязательно говорить о представлении контейнера встраивать SEGUE, что вы видите в IB, но это может быть просто простой 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]]; 

С помощью этого процесса, вы избежите восстановление любые сложные ограничения, которые ранее могли продиктовать взаимосвязь замещенного представления со всеми его прежними одноранговыми узлами в иерархии представлений.

Смежные вопросы