Проблемы с адаптивным интерфейсом iOS Autolayout
Я хочу изменить свой макет в зависимости от того, будет ли ширина больше высоты. Но я продолжаю получать предупреждения макета. Я смотрю оба видео WWDC по адаптивной верстке, которая очень помогла, но не решила проблему. Я создал простую версию моего макета, используя следующий код. Это только для того, чтобы люди могли воспроизвести мою проблему. Код запускается на iPhone 7 плюс.
import UIKit
class ViewController: UIViewController {
var view1: UIView!
var view2: UIView!
var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
view1 = UIView()
view1.translatesAutoresizingMaskIntoConstraints = false
view1.backgroundColor = UIColor.red
view2 = UIView()
view2.translatesAutoresizingMaskIntoConstraints = false
view2.backgroundColor = UIColor.green
view.addSubview(view1)
view.addSubview(view2)
layoutPortrait()
}
func layoutPortrait() {
let views = [
"a1": view1,
"a2": view2
]
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a1]|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a2]|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)
NSLayoutConstraint.activate(constraints)
}
func layoutLandscape() {
let views = [
"a1": view1,
"a2": view2
]
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a1(a2)][a2(a1)]|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1]|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a2]|", options: [], metrics: nil, views: views)
NSLayoutConstraint.activate(constraints)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
NSLayoutConstraint.deactivate(constraints)
constraints.removeAll()
if (size.width > size.height) {
layoutLandscape()
} else {
layoutPortrait()
}
}
}
Когда я поворачиваю пару раз, xcode записывает предупреждения. Я думаю, что я переключаю макет на ранний, потому что он все еще меняется, но я уже установил высоту в портретном режиме на большую или что-то еще. Кто-то знает, что я делаю не так?
Предупреждения об ограничениях: (происходит при переходе от пейзажа к портрету)
[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:0x618000081900 V:|-(0)-[UIView:0x7ff68ee0ac40] (active, names: '|':UIView:0x7ff68ec03f50 )>",
"<NSLayoutConstraint:0x618000081d60 UIView:0x7ff68ee0ac40.height == 450 (active)>",
"<NSLayoutConstraint:0x618000083cf0 V:[UIView:0x7ff68ee0ac40]-(0)-[UIView:0x7ff68ee0ade0] (active)>",
"<NSLayoutConstraint:0x618000083d40 V:[UIView:0x7ff68ee0ade0]-(0)-| (active, names: '|':UIView:0x7ff68ec03f50 )>",
"<NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x618000081d60 UIView:0x7ff68ee0ac40.height == 450 (active)>
4 ответа
Вы правы, вы делаете работу с ограничениями слишком рано. <NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414
... это созданное системой ограничение высоты представления контроллера представления в альбомной ориентации - почему это 414, то есть высота iPhone 5.5 (6/7 Plus) в альбомной ориентации. Поворот еще не начался, вертикальные ограничения с конфликтом ограничений 450, и вы получите предупреждение.
Таким образом, вы должны добавить ограничения после завершения вращения. viewWillLayoutSubviews
это логическое место, так как оно вызывается, когда размер представления готов, но до того, как что-либо появится на экране, и может спасти систему, которую он бы сделал.
Однако, чтобы заставить замолчать предупреждение, вам все равно нужно снять ограничения в viewWillTransition(to size: with coordinator:)
, Система делает новые автоматические расчеты макета перед viewWillLayoutSubviews()
называется, и вы получите предупреждение в другую сторону, при переходе от портрета к ландшафту.
РЕДАКТИРОВАТЬ: как @sulthan отметил в комментарии, так как другие вещи могут вызвать макет, вы также должны всегда удалять ограничения перед их добавлением. Если [constraints]
массив был опустошен, эффекта нет. Поскольку очистка массива после деактивации является критически важным шагом, он должен выполняться собственным методом, поэтому clearConstraints()
,
Также не забудьте проверить ориентацию в исходной раскладке - звоните layoutPortrait()
в viewDidLoad()
думал, конечно, что это не проблема.
Новые / изменить методы:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// Should be inside layoutPortrait/Landscape methods, here to keep code sample short.
clearConstraints()
if (self.view.frame.size.width > self.view.frame.size.height) {
layoutLandscape()
} else {
layoutPortrait()
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
clearConstraints()
}
/// Ensure array is emptied when constraints are deactivated. Can be called repeatedly.
func clearConstraints() {
NSLayoutConstraint.deactivate(constraints)
constraints.removeAll()
}
Если проблема с макетом связана с фиксированным (или программно измененным) ограничением ширины или высоты, конфликтующим с UIView-Encapsulated-Layout-Width
или же UIView-Encapsulated-Layout-Height
проблема заключается в том, что это нерушимое ограничение вызывает слишком большое ограничение (приоритет 1000).
Вы часто можете решить проблему, просто понизив приоритет вашего конфликтующего ограничения (возможно, просто до 999), который учтет любой временный конфликт, пока вращение не будет завершено, и последующий макет продолжит работу с нужной вам шириной или высотой.
Я пришел с тем же ответом, что и @Samantha. Мне не нужно было звонить deactivate
до звонка super
, тем не мение. Это альтернатива с использованием коллекций признаков, но transitionToSize
подход работает аналогично.
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
NSLayoutConstraint.deactivate(constraints)
constraints.removeAll()
coordinator.animate(alongsideTransition: { context in
if newCollection.verticalSizeClass == .compact {
self.layoutLandscape()
} else {
self.layoutPortrait()
}
}, completion: nil)
}
Это проблемная часть:
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)
Эта строка генерирует 4 ограничения:
- сверху к
a1
расстояние0
, - высота
a1
является450
, a1
вa2
расстояние0
,a2
до нижнего расстояния0
,
Четыре ограничения вместе ломаются в ландшафтном режиме, потому что общая высота экрана будет меньше 450
,
Сейчас вы обновляете ограничения перед переходом в альбомный режим, и это работает, когда вы переключаетесь из портретного в ландшафтный режим. Но когда вы переключаетесь с пейзажа на портрет, вы слишком рано меняете свои ограничения, и на короткое время вы получаете портретные ограничения, пока находитесь в пейзаже, вызывая это предупреждение.
Обратите внимание, что в ваших ограничениях нет ничего плохого. Если вы хотите исправить предупреждение, на мой взгляд, лучшим решением будет уменьшить приоритет одного из ваших ограничений, например ограничения высоты:
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450@999)][a2]|", options: [], metrics: nil, views: views)
Насколько я могу судить, предупреждение на самом деле потому, что ограничение перемещается слишком поздно. Предупреждение появляется при повороте в альбомную ориентацию - ограничение 450 все еще установлено и не может быть выполнено, потому что высота вида слишком мала.
Я попробовал несколько вещей, и что, казалось, работало лучше всего, это установило ограничение для меньшего вида, таким образом, чтобы высота большего вида была все еще 450.
Скорее, чем
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)
я использовал
let height = view.frame.height - 450
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1][a2(\(height))]|", options: [], metrics: nil, views: views)
а затем в viewWillTransitionToSize
:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
NSLayoutConstraint.deactivate(self.constraints)
self.constraints.removeAll()
coordinator.animate(alongsideTransition: { (context) in
if (size.width > size.height) {
self.layoutLandscape()
} else {
self.layoutPortrait()
}
}, completion: nil)
}
Редактировать:
Другая вещь, которая сработала для меня (и, возможно, более общий ответ, хотя я не уверен, что это рекомендуется): деактивация ограничений перед вызовом super.viewWillTransition
а затем анимация отдыха вместе с переходом. Я полагаю, что ограничения удаляются немедленно, технически до начала поворота, и поэтому, как только приложение начинает вращаться в горизонтальной плоскости (в этот момент старое ограничение высоты было бы неудовлетворительным), ограничения уже деактивированы, и новые могут принять эффект.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
NSLayoutConstraint.deactivate(constraints)
super.viewWillTransition(to: size, with: coordinator)
constraints.removeAll()
coordinator.animate(alongsideTransition: { (context) in
if (size.width > size.height) {
self.layoutLandscape()
} else {
self.layoutPortrait()
}
}, completion: nil)
}