AutoLayout со скрытыми UIViews?


164

Я чувствую, что это довольно распространенная парадигма для показа / скрытия UIViews, чаще всего UILabels, в зависимости от бизнес-логики. Мой вопрос заключается в том, как лучше всего использовать AutoLayout для ответа на скрытые представления, как если бы их кадр был 0x0. Вот пример динамического списка 1-3 функций.

Динамический список возможностей

Прямо сейчас у меня есть 10px верхнее пространство от кнопки до последней метки, которая, очевидно, не будет скользить вверх, когда метка скрыта. На данный момент я создал выход для этого ограничения и изменил константу в зависимости от того, сколько меток я показываю. Это явно немного странно, так как я использую отрицательные значения констант, чтобы нажимать кнопку над скрытыми кадрами. Это также плохо, потому что он не ограничивается фактическими элементами макета, а просто скрытными статическими вычислениями, основанными на известных высотах / заполнении других элементов, и, очевидно, борется с тем, для чего был создан AutoLayout.

Очевидно, я мог бы просто создавать новые ограничения в зависимости от моих динамических меток, но это много микроуправления и многословия для попытки просто разрушить некоторые пробелы. Есть ли лучшие подходы? Изменить размер кадра 0,0 и позволить AutoLayout работать без ограничений? Удаление просмотров полностью?

Честно говоря, просто изменение константы из контекста скрытого представления требует одной строки кода с простым вычислением. Воссоздание новых ограничений с constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant: кажется таким тяжелым.

Редактировать февраль 2018 : ответ знакомства Бен с UIStackViewс


Спасибо Райан за этот вопрос. Я сходил с ума, что делать для случаев, как вы просили. Каждый раз, когда я проверяю учебник для autolayout, большинство из них говорит, что ссылаются на сайт учебника raywenderlich, который я нахожу немного трудным разобрать.
Нассиф

Ответы:


82

UIStackViewВероятно, это путь для iOS 9+. Он не только обрабатывает скрытый вид, но также удаляет дополнительные интервалы и поля при правильной настройке.


8
Будьте осторожны при использовании UIStackView в UITableViewCell. Для меня, как только представление стало очень сложным, прокрутка начала становиться прерывистой, где она заикалась при каждой прокрутке ячейки. Под прикрытием StackView добавляет / удаляет ограничения, и это не обязательно будет достаточно эффективно для плавной прокрутки.
Кенто

7
Было бы неплохо привести небольшой пример того, как с этим справляется стековое представление.
lostintranslation

@ Кенто, это все еще проблема на iOS 10?
Crashalot

3
Пять лет спустя ... я должен обновить и установить это как принятый ответ? Это было доступно для трех циклов выпуска сейчас.
Райан Романчук

1
@RyanRomanchuk Вы должны, это, безусловно, лучший ответ сейчас. Спасибо Бен!
Дункан Лук

234

Лично я предпочитаю показывать / скрывать виды, чтобы создать IBOutlet с соответствующим ограничением ширины или высоты.

Затем я обновляю constantзначение, чтобы 0скрыть, или любое другое значение, чтобы показать.

Большим преимуществом этого метода является то, что относительные ограничения будут сохранены. Например, допустим, у вас есть вид A и вид B с горизонтальным зазором x . Когда view A width constantустановлен в0.f вид B переместится влево, чтобы заполнить это пространство.

Нет необходимости добавлять или удалять ограничения, что является тяжелой операцией. Простое обновление ограничений constantсделает свое дело.


1
именно. Пока - в этом примере - вы располагаете вид B справа от вида A, используя горизонтальный зазор. Я использую эту технику все время, и она работает очень хорошо
Макс МакЛауд

9
@MaxMacLeod Просто чтобы убедиться: если промежуток между двумя представлениями равен x, чтобы представление B начиналось с позиции представления A, мы также должны изменить константу ограничения промежутка на 0, верно?
Kernix

1
@kernix да, если между двумя представлениями потребуется ограничение по горизонтали. константа 0, конечно, означает отсутствие разрыва. Таким образом, скрытие вида A путем установки его ширины в 0 переместит вид B влево, чтобы он был на одном уровне с контейнером. Кстати, спасибо за голоса!
Макс МакЛеод

2
@MaxMacLeod Спасибо за ваш ответ. Я попытался сделать это с UIView, который содержит другие представления, но подпредставления этого представления не скрываются, когда я устанавливаю его ограничение высоты 0. Следует ли это решение работать с представлениями, содержащими подпредставления? Спасибо!
Shaunlim

6
Был бы также признателен за решение, которое работает для UIView, содержащий другие представления ...
Марк Гибо

96

Решение использовать константу, 0когда она скрыта, и другую константу, если вы показываете ее снова, является функциональным, но это неудовлетворительно, если ваш контент имеет гибкий размер. Вам нужно измерить ваш гибкий контент и установить постоянную обратно. Это кажется неправильным и имеет проблемы, если размер содержимого изменяется из-за событий сервера или пользовательского интерфейса.

У меня есть лучшее решение.

Идея состоит в том, чтобы установить правило высоты 0 для высокого приоритета, когда мы скрываем элемент, чтобы он не занимал пространство для автоматической разметки.

Вот как вы это делаете:

1. установите ширину (или высоту) 0 в конструкторе интерфейсов с низким приоритетом.

настройка ширины 0 правило

Интерфейсный Разработчик не будет кричать о конфликтах, потому что приоритет низкий. Проверьте поведение высоты, временно установив приоритет 999 (1000 запрещается программно изменять, поэтому мы не будем его использовать). Разработчик интерфейса, вероятно, теперь будет кричать о конфликтующих ограничениях. Вы можете исправить это, установив приоритеты для связанных объектов на 900 или около того.

2. Добавьте выход, чтобы вы могли изменить приоритет ограничения ширины в коде:

выходное соединение

3. Настройте приоритет, когда вы скрываете свой элемент:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999

@SimplGy, скажите, пожалуйста, что это за место?
Niharika

Это не является частью общего решения, @Niharika, это просто то, что было бизнес-правилом в моем случае (покажите представление, если ресторан скоро закрывается).
SimplGy

11

В этом случае я сопоставляю высоту метки Author с соответствующим IBOutlet:

@property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;

и когда я устанавливаю высоту ограничения равной 0.0f, мы сохраняем «отступ», потому что высота кнопки воспроизведения позволяет это сделать.

cell.authorLabelHeight.constant = 0; //Hide 
cell.authorLabelHeight.constant = 44; //Show

введите описание изображения здесь


9

Здесь есть много решений, но мой обычный подход снова отличается :)

Установите два набора ограничений, аналогичных ответу Хорхе Аримани и TMin:

введите описание изображения здесь

Все три отмеченных ограничения имеют одинаковое значение для Константы. Ограничения, отмеченные A1 и A2, имеют свой приоритет, установленный на 500, в то время как ограничение, отмеченное B, имеет свой приоритет, установленный на 250 (или UILayoutProperty.defaultLowв коде).

Подключите ограничение B к IBOutlet. Затем, когда вы скрываете элемент, вам просто нужно установить высокий приоритет ограничения (750):

constraintB.priority = .defaultHigh

Но когда элемент виден, установите приоритет обратно на низкий (250):

constraintB.priority = .defaultLow

Преимущество этого подхода (по общему признанию, незначительное) перед простым изменением isActiveограничения B состоит в том, что у вас все еще есть рабочее ограничение, если временный элемент удаляется из представления другими средствами.


Это имеет то преимущество, что в Storyboard и в коде нет повторяющихся значений, таких как высота элемента / ширина. Очень элегантно.
Робин Догерти

Спасибо!! Это помогло мне
Parth Барот

Отличный подход, спасибо
Василий

5

Подкласс просмотра и переопределения func intrinsicContentSize() -> CGSize. Просто вернитесь, CGSizeZeroесли вид скрыт.


2
Это круто. Некоторым элементам пользовательского интерфейса необходим триггер invalidateIntrinsicContentSize. Вы можете сделать это переопределено setHidden.
DrMickeyLauer

5

Я только что узнал, что для того, чтобы UILabel не занимал пространство, вы должны скрыть его И установить его текст в пустую строку. (iOS 9)

Знание этого факта / ошибки может помочь некоторым людям упростить их макеты, возможно, даже те, которые были в оригинальном вопросе, поэтому я решил опубликовать его.


1
Я не думаю, что вам нужно это скрывать. Установка его текста в пустую строку должно быть достаточно.
Роб VS

4

Я удивлен, что не существует более элегантного подхода UIKit для этого желаемого поведения . Кажется, это очень распространенная вещь, чтобы хотеть быть в состоянии сделать.

Поскольку подключение ограничений к IBOutletsих константам и установка их на состояние « 0неприятное» (и вызывало NSLayoutConstraintпредупреждения, когда у вашего представления были подпредставления), я решил создать расширение, которое дает простой подход с отслеживанием состояния для скрытия / показа UIViewобъекта с ограничениями Auto Layout.

Он просто скрывает вид и удаляет внешние ограничения. Когда вы снова показываете представление, оно добавляет ограничения обратно. Единственное предостережение в том, что вам нужно будет указать гибкие ограничения отработки отказа для окружающих видов.

Редактировать Этот ответ предназначен для iOS 8.4 и ниже. В iOS 9 просто используйте UIStackViewподход.


1
Это мой предпочтительный подход тоже. Это значительное улучшение по сравнению с другими ответами здесь, поскольку оно не просто сводит представление к нулевому размеру. У сквош-подхода будет много проблем, кроме довольно тривиальных выплат. Примером этого является большой разрыв, где скрытый элемент находится в этом другом ответе . И вы можете в конечном итоге с нарушениями ограничений, как уже упоминалось.
Benjohn

@DanielSchlaug Спасибо за указание на это. Я обновил лицензию до MIT. Тем не менее, я требую умственного дурака каждый раз, когда кто-то реализует это решение.
Альберт Бори

4

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

Убедитесь, что ваши свойства strong

в коде вы просто должны установить константу равной 0 и активировать ее, скрыть содержимое или деактивировать его, чтобы показать содержимое. Это лучше, чем связываться с константой и ее сохранением-восстановлением. Не забудьте позвонить layoutIfNeededпотом.

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

@property (strong, nonatomic) IBOutlet UIView *myContainer;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *myContainerHeight; //should be strong!!

-(void) showContainer
{
    self.myContainerHeight.active = NO;
    self.myContainer.hidden = NO;
    [self.view layoutIfNeeded];
}
-(void) hideContainer
{
    self.myContainerHeight.active = YES;
    self.myContainerHeight.constant = 0.0f;
    self.myContainer.hidden = YES;
    [self.view layoutIfNeeded];
}

Как только у вас есть настройки, вы можете проверить их в IntefaceBuilder, установив ограничение на 0, а затем вернувшись к исходному значению. Не забудьте проверить приоритеты других ограничений, чтобы при скрытии не возникало никаких конфликтов. Другой способ проверить это установить 0 и установить приоритет 0, но вы не должны забывать восстановить его с наивысшим приоритетом.


1
Это связано с тем, что ограничение и представление не всегда могут быть сохранены иерархией представления, поэтому, если вы деактивируете ограничение и скрываете представление, вы все равно сохраняете их, чтобы отображать их снова независимо от того, какую иерархию представлений решит сохранить или нет.
Хорхе Аримани

3

Я строю категорию, чтобы легко обновлять ограничения:

[myView1 hideByHeight:YES];

Ответ здесь: Скрыть autolayout UIView: Как получить существующий NSLayoutConstraint, чтобы обновить этот

введите описание изображения здесь


8
К сожалению, вы получаете двойной интервал там, где указывает желтая стрелка.
Золтан

Как и с другими решениями выше, я думаю. Совсем не идеально.
Benjohn

2

Мой предпочтительный метод очень похож на предложенный Хорхе Аримани.

Я предпочитаю создавать несколько ограничений. Сначала создайте свои ограничения, когда вторая метка видна. Создайте выход для ограничения между кнопкой и второй меткой (если вы используете objc, убедитесь, что он сильный). Это ограничение определяет высоту между кнопкой и второй меткой, когда она видна.

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

Наконец, когда вы скрываете вторую метку, переключаете .isActiveсвойство этих ограничений и вызываетеsetNeedsDisplay()

И это все, никаких магических чисел, никакой математики, если у вас есть несколько ограничений на включение и выключение, вы даже можете использовать коллекции розеток, чтобы держать их организованными по состоянию. (AKA хранит все скрытые ограничения в одной коллекции OutletCollection и не скрытые ограничения в другой и просто перебирает каждую коллекцию, переключая их статус .isActive).

Я знаю, что Райан Романчук сказал, что он не хотел использовать множественные ограничения, но я чувствую, что это не micromanage-y, а проще, чем динамическое создание представлений и ограничений программно (чего, я думаю, он хотел бы избежать, если бы я читаю вопрос правильно).

Я создал простой пример, надеюсь, он будет полезен ...

import UIKit

class ViewController: UIViewController {

    @IBOutlet var ToBeHiddenLabel: UILabel!


    @IBOutlet var hiddenConstraint: NSLayoutConstraint!
    @IBOutlet var notHiddenConstraint: NSLayoutConstraint!


    @IBAction func HideMiddleButton(_ sender: Any) {

        ToBeHiddenLabel.isHidden = !ToBeHiddenLabel.isHidden
        notHiddenConstraint.isActive = !notHiddenConstraint.isActive
        hiddenConstraint.isActive = !hiddenConstraint.isActive

        self.view.setNeedsDisplay()
    }
}

введите описание изображения здесь


0

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

Мой метод удаляет все представления (в моем случае экземпляры UIImageView), выбирает, какие из них необходимо добавить обратно, и в цикле добавляет обратно каждое и создает новые ограничения. Это на самом деле очень просто, пожалуйста, следуйте. Вот мой быстрый и грязный код для этого:

// remove all views
[self.twitterImageView removeFromSuperview];
[self.localQuestionImageView removeFromSuperview];

// self.recipients always has to be present
NSMutableArray *items;
items = [@[self.recipients] mutableCopy];

// optionally add the twitter image
if (self.question.sharedOnTwitter.boolValue) {
    [items addObject:self.twitterImageView];
}

// optionally add the location image
if (self.question.isLocal) {
    [items addObject:self.localQuestionImageView];
}

UIView *previousItem;
UIView *currentItem;

previousItem = items[0];
[self.contentView addSubview:previousItem];

// now loop through, add the items and the constraints
for (int i = 1; i < items.count; i++) {
    previousItem = items[i - 1];
    currentItem = items[i];

    [self.contentView addSubview:currentItem];

    [currentItem mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(previousItem.mas_centerY);
        make.right.equalTo(previousItem.mas_left).offset(-5);
    }];
}


// here I just connect the left-most UILabel to the last UIView in the list, whichever that was
previousItem = items.lastObject;

[self.userName mas_remakeConstraints:^(MASConstraintMaker *make) {
    make.right.equalTo(previousItem.mas_left);
    make.leading.equalTo(self.text.mas_leading);
    make.centerY.equalTo(self.attachmentIndicator.mas_centerY);;
}];

Я получаю чистый, последовательный макет и расстояние. Мой код использует Masonry, я настоятельно рекомендую его: https://github.com/SnapKit/Masonry


0

Попробуйте BoxView , он делает динамический макет кратким и читабельным.
В вашем случае это:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.