Вложенные UIStackViews Broken Constraints
У меня сложная иерархия представлений, построенная в Interface Builder, с вложенными UIStackViews. Я получаю уведомления об "неудовлетворительных ограничениях" каждый раз, когда я скрываю некоторые из своих внутренних стеков. Я отслеживал это:
(
"<NSLayoutConstraint:0x1396632d0 'UISV-canvas-connection' UIStackView:0x1392c5020.top == UILabel:0x13960cd30'Also available on iBooks'.top>",
"<NSLayoutConstraint:0x139663470 'UISV-canvas-connection' V:[UIButton:0x139554f80]-(0)-| (Names: '|':UIStackView:0x1392c5020 )>",
"<NSLayoutConstraint:0x139552350 'UISV-hiding' V:[UIStackView:0x1392c5020(0)]>",
"<NSLayoutConstraint:0x139663890 'UISV-spacing' V:[UILabel:0x13960cd30'Also available on iBooks']-(8)-[UIButton:0x139554f80]>"
)
В частности, ограничение UISV-spacing
: при скрытии UIStackView его высокое ограничение получает константу 0, но похоже, что она сталкивается с внутренним ограничением интервала стеков: она требует 8 точек между моей меткой и кнопкой, что непримиримо с скрывая ограничение, и поэтому сбой ограничений.
Есть ли способ обойти это? Я попытался рекурсивно скрыть все внутренние StackViews скрытого представления стека, но это приводит к странной анимации, когда контент выплывает из экрана и вызывает серьезные потери FPS для загрузки, но при этом не устраняет проблему.
Ответы
Ответ 1
В идеале мы могли бы просто установить приоритет ограничения UISV-spacing
на более низкое значение, но, похоже, нет никакого способа сделать это.:)
У меня есть успех, устанавливающий свойство spacing
вложенных представлений стека до 0 перед тем, как скрыть, и восстановление до правильного значения после повторного его отображения.
Я думаю, что это будет рекурсивно работать над представлениями вложенных стеков. Вы можете сохранить исходное значение свойства spacing
в словаре и восстановить его позже.
Мой проект имеет только один уровень вложенности, поэтому я не уверен, что это приведет к проблемам с FPS. Пока вы не изменяете изменения в интервалах, я не думаю, что это создало бы слишком много хитов.
Ответ 2
Это известная проблема с скрытием вложенных представлений стека.
Существуют 3 решения этой проблемы:
- Измените интервал на 0, но тогда вам нужно будет запомнить предыдущее значение расстояния.
- Вызовите
innerStackView.removeFromSuperview()
, но тогда вам нужно будет запомнить, куда вставить представление стека.
- Оберните представление стека в UIView с хотя бы одним ограничением 999. Например. top @1000, ведущий @1000, конечный @1000, внизу @999.
Третий вариант - лучший, на мой взгляд. Для получения дополнительной информации об этой проблеме, о том, почему это происходит, о различных решениях и о том, как реализовать решение 3, см. мой ответ на аналогичный вопрос.
Ответ 3
Я столкнулся с аналогичной проблемой с UISV-скрытием. Для меня решение заключалось в том, чтобы уменьшить приоритеты моих собственных ограничений от Required (1000) до чего-то меньшего. Когда добавляются ограничения скрытия UISV, они принимают приоритет, и ограничения больше не конфликтуют.
Ответ 4
Итак, у вас есть это:
![сломанная анимация]()
И проблема в том, что когда вы сначала сворачиваете внутренний стек, вы получаете ошибки автоматического макета:
2017-07-02 15:40:02.377297-0500 nestedStackViews[17331:1727436] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x62800008ce90 'UISV-canvas-connection' UIStackView:0x7fa57a70fce0.top == UILabel:0x7fa57a70ffb0'Top Label of Inner Stack'.top (active)>",
"<NSLayoutConstraint:0x62800008cf30 'UISV-canvas-connection' V:[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...']-(0)-| (active, names: '|':UIStackView:0x7fa57a70fce0 )>",
"<NSLayoutConstraint:0x62000008bc70 'UISV-hiding' UIStackView:0x7fa57a70fce0.height == 0 (active)>",
"<NSLayoutConstraint:0x62800008cf80 'UISV-spacing' V:[UILabel:0x7fa57a70ffb0'Top Label of Inner Stack']-(8)-[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...'] (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x62800008cf80 'UISV-spacing' V:[UILabel:0x7fa57a70ffb0'Top Label of Inner Stack']-(8)-[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...'] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
Проблема, как вы заметили, заключается в том, что внешний вид стека применяет ограничение height = 0 к представлению внутреннего стека. Это конфликтует с 8-кратным ограничением заполнения, применяемым представлением внутреннего стека между его собственными подзонами. Оба ограничения не могут выполняться одновременно.
Внешний вид стека использует это ограничение height = 0, я считаю, потому что он выглядит лучше, когда он анимирован, чем просто позволяет скрывать внутренний вид, не сокращаясь сначала.
Там простое исправление: оберните представление внутреннего стека в простой UIView
и скройте эту оболочку. Я продемонстрирую.
Здесь схема сцены для сломанной версии выше:
![сломанная схема]()
Чтобы устранить проблему, выберите представление внутреннего стека. В строке меню выберите "Редактор" > "Вставить" > "Вид":
![embed in view]()
Интерфейс Builder создал ограничение ширины в представлении оболочки, когда я это сделал, поэтому удалите ограничение ширины:
![delete width constraint]()
Затем создайте ограничения между всеми четырьмя краями обертки и представлением внутреннего стека:
![создать ограничения]()
В этот момент макет фактически корректен во время выполнения, но Interface Builder рисует его неправильно. Вы можете исправить это, установив приоритеты вертикального обнимания детей внутреннего стека выше. Я установил их на 800:
![обнимающие приоритеты]()
На данный момент мы на самом деле не зафиксировали неудовлетворительную проблему ограничения. Для этого найдите нижнее ограничение, которое вы только что создали, и установите его приоритет менее необходимым. Позвольте изменить его на 800:
![изменить приоритет нижнего ограничения]()
Наконец, вы предположительно имели выход в своем контроллере вида, подключенного к представлению внутреннего стека, потому что вы меняли его свойство hidden
. Измените эту розетку для подключения к представлению обертки вместо представления внутреннего стека. Если тип вашего выхода UIStackView
, вам нужно изменить его на UIView
. Мой был уже типа UIView
, поэтому я только что подключил его в раскадровке:
![change outlet]()
Теперь, когда вы переключаете представление оболочки hidden
, представление стека будет свернуто, без каких-либо неудовлетворительных предупреждений ограничений. Он выглядит практически идентичным, поэтому я не буду беспокоиться о публикации другого GIF приложения.
Вы можете найти мой тестовый проект в этом репозитории github.
Ответ 5
Здесь реализована реализация предложения Senseful № 3, написанного как класс Swift 3 с использованием ограничений SnapKit. Я также попытался переопределить свойства, но никогда не работал без предупреждений, поэтому я буду придерживаться оболочки UIStackView:
class NestableStackView: UIView {
private var actualStackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame);
addSubview(actualStackView);
actualStackView.snp.makeConstraints { (make) in
// Lower edges priority to allow hiding when spacing > 0
make.edges.equalToSuperview().priority(999);
}
}
convenience init() {
self.init(frame: CGRect.zero);
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addArrangedSubview(_ view: UIView) {
actualStackView.addArrangedSubview(view);
}
func removeArrangedSubview(_ view: UIView) {
actualStackView.removeArrangedSubview(view);
}
var axis: UILayoutConstraintAxis {
get {
return actualStackView.axis;
}
set {
actualStackView.axis = newValue;
}
}
open var distribution: UIStackViewDistribution {
get {
return actualStackView.distribution;
}
set {
actualStackView.distribution = newValue;
}
}
var alignment: UIStackViewAlignment {
get {
return actualStackView.alignment;
}
set {
actualStackView.alignment = newValue;
}
}
var spacing: CGFloat {
get {
return actualStackView.spacing;
}
set {
actualStackView.spacing = newValue;
}
}
}
Ответ 6
Другой подход
Попробуйте избежать вложенных UIStackViews. Я люблю их и строю почти все с ними. Но поскольку я понял, что они тайно добавляют ограничения, я стараюсь использовать их только на самом высоком уровне и, если это возможно, не вложенные. Таким образом, я могу указать второй наивысший приоритет .defaultHigh
на ограничение расстояния, которое разрешает мои предупреждения.
Этот приоритет достаточно для предотвращения большинства проблем с макетами.
Конечно, вам нужно указать еще несколько ограничений, но таким образом вы полностью контролируете их и четко представляете свой макет представления.