Невозможно исправить анимацию Auto Layout во время события поворота

Не бойтесь огромного кода, который будет следовать за ним. Вы можете скопировать и вставить фрагмент кода в новое приложение с одним представлением, чтобы посмотреть, как он себя ведет. Проблема находится где-то внутри блока завершения анимации, выполняемой вместе с анимацией вращения.

import UIKit

let sizeConstant: CGFloat = 60

class ViewController: UIViewController {

    let topView = UIView()
    let backgroundView = UIView()
    let stackView = UIStackView()
    let lLayoutGuide = UILayoutGuide()
    let bLayoutGuide = UILayoutGuide()
    var bottomConstraints = [NSLayoutConstraint]()
    var leftConstraints = [NSLayoutConstraint]()

    var bLayoutHeightConstraint: NSLayoutConstraint!
    var lLayoutWidthConstraint: NSLayoutConstraint!

    override func viewDidLoad() {

        super.viewDidLoad()

        print(UIScreen.main.bounds)

        //        self.view.layer.masksToBounds = true

        let views = [
            UIButton(type: .infoDark),
            UIButton(type: .contactAdd),
            UIButton(type: .detailDisclosure)
        ]
        views.forEach(self.stackView.addArrangedSubview)

        self.backgroundView.backgroundColor = UIColor.red
        self.backgroundView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(self.backgroundView)

        self.topView.backgroundColor = UIColor.green
        self.topView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(self.topView)

        self.stackView.axis = isPortrait() ? .horizontal : .vertical
        self.stackView.distribution = .fillEqually
        self.stackView.translatesAutoresizingMaskIntoConstraints = false
        self.backgroundView.addSubview(self.stackView)

        self.topView.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor).isActive = true
        self.topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        self.topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        self.topView.heightAnchor.constraint(equalToConstant: 46).isActive = true

        self.view.addLayoutGuide(self.lLayoutGuide)
        self.view.addLayoutGuide(self.bLayoutGuide)

        self.bLayoutGuide.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        self.bLayoutGuide.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        self.bLayoutGuide.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        self.bLayoutHeightConstraint = self.bLayoutGuide.heightAnchor.constraint(equalToConstant: isPortrait() ? sizeConstant : 0)
        self.bLayoutHeightConstraint.isActive = true

        self.lLayoutGuide.topAnchor.constraint(equalTo: self.topView.bottomAnchor).isActive = true
        self.lLayoutGuide.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        self.lLayoutGuide.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        self.lLayoutWidthConstraint = self.lLayoutGuide.widthAnchor.constraint(equalToConstant: isPortrait() ? 0 : sizeConstant)
        self.lLayoutWidthConstraint.isActive = true

        self.stackView.topAnchor.constraint(equalTo: self.backgroundView.topAnchor).isActive = true
        self.stackView.bottomAnchor.constraint(equalTo: self.backgroundView.bottomAnchor).isActive = true
        self.stackView.leadingAnchor.constraint(equalTo: self.backgroundView.leadingAnchor).isActive = true
        self.stackView.trailingAnchor.constraint(equalTo: self.backgroundView.trailingAnchor).isActive = true

        self.bottomConstraints = [
            self.backgroundView.topAnchor.constraint(equalTo: self.bLayoutGuide.topAnchor),
            self.backgroundView.leadingAnchor.constraint(equalTo: self.bLayoutGuide.leadingAnchor),
            self.backgroundView.trailingAnchor.constraint(equalTo: self.bLayoutGuide.trailingAnchor),
            self.backgroundView.heightAnchor.constraint(equalToConstant: sizeConstant)
        ]

        self.leftConstraints = [
            self.backgroundView.topAnchor.constraint(equalTo: self.lLayoutGuide.topAnchor),
            self.backgroundView.bottomAnchor.constraint(equalTo: self.lLayoutGuide.bottomAnchor),
            self.backgroundView.trailingAnchor.constraint(equalTo: self.lLayoutGuide.trailingAnchor),
            self.backgroundView.widthAnchor.constraint(equalToConstant: sizeConstant)
        ]

        if isPortrait() {

            NSLayoutConstraint.activate(self.bottomConstraints)

        } else {

            NSLayoutConstraint.activate(self.leftConstraints)
        }
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

        let willBePortrait = size.width < size.height

        coordinator.animate(alongsideTransition: {

            context in

            let halfDuration = context.transitionDuration / 2.0

            UIView.animate(withDuration: halfDuration, delay: 0, options: .overrideInheritedDuration, animations: {

                self.bLayoutHeightConstraint.constant = 0
                self.lLayoutWidthConstraint.constant = 0
                self.view.layoutIfNeeded()

            }, completion: {

                _ in

                // HERE IS THE ISSUE!

                // Putting this inside `performWithoutAnimation` did not helped
                if willBePortrait {

                    self.stackView.axis = .horizontal
                    NSLayoutConstraint.deactivate(self.leftConstraints)
                    NSLayoutConstraint.activate(self.bottomConstraints)

                } else {

                    self.stackView.axis = .vertical
                    NSLayoutConstraint.deactivate(self.bottomConstraints)
                    NSLayoutConstraint.activate(self.leftConstraints)
                }
                self.view.layoutIfNeeded()

                UIView.animate(withDuration: halfDuration) {

                    if willBePortrait {

                        self.bLayoutHeightConstraint.constant = sizeConstant

                    } else {

                        self.lLayoutWidthConstraint.constant = sizeConstant
                    }
                    self.view.layoutIfNeeded()
                }
            })
        })

        super.viewWillTransition(to: size, with: coordinator)
    }

    func isPortrait() -> Bool {

        let size = UIScreen.main.bounds.size
        return size.width < size.height
    }
}

Вот несколько снимков экрана, которые я не могу решить. Посмотрите внимательно на углы:

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

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

PS: Лучший способ проверить это симулятор iPhone X Plus.

Ответы

Ответ 1

Использовать классы размеров

Совершенно другой подход к плавной анимации анимации панели инструментов заключается в том, чтобы использовать классы размера автозапуска, в частности hR (высота Обычный) и hC (высота Compact) и создайте для каждого из них разные ограничения.

Horizontal to vertical
↻ воспроизведение анимации

  • Дальнейшее улучшение состоит в том, чтобы фактически использовать две различные панели инструментов: одну для вертикального дисплея и одну для горизонтальной. Это не какое-то среднее требование, но оно решает изменение размера самой панели инструментов (†).

  • Окончательное уточнение заключается в реализации этих изменений в Interface Builder, дающих ровно 0 строк кода, что, конечно же, не является обязательным.

Vertical to Horizontal
↻ воспроизведение анимации


Нулевые строки кода

Ни одно из предлагаемых решений не требует использования UIViewControllerTransitionCoordinator, которое не только значительно упрощает разработку и обслуживание исходного кода, но также не требует использования жестко заданных значений или поддерживающих утилит. Вы также получаете предварительный просмотр в Конструкторе интерфейсов. И как только он будет завершен в IB, вы все равно сможете преобразовать логику в программирование во время выполнения, если это абсолютное требование.

  • Обратите внимание, что UIStackView встроен в панель инструментов и, следовательно, следует за анимацией. Вы можете контролировать количество колебания панелей инструментов вне поля зрения постоянным; Я выбрал 1024, чтобы они быстро выходили из экрана и возвращались снова в конце перехода.

    Smooth

  • (†) Дальнейшее использование интерфейсов Builder и классов размеров, вы все равно можете использовать одну панель инструментов, но если вы это сделаете, она изменит размер во время перехода. Опять же, UIStackView встроен, а его ориентация тоже зависит от классов размеров, а ОС обрабатывает всю анимацию без необходимости создания координатора:

    классы классов

    Smooth too


► Найдите это решение на GitHub и дополнительные сведения о Быстрые рецепты.

Ответ 2

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

Это не происходит, потому что вы активируете/деактивируете свои ограничения внутри метода coordinator.animate(alongsideTransition:completion:). Когда вы используете этот метод, все внутри блока анимации будет анимироваться вместе с вашим контроллером контроллера, поэтому немедленных привязок к руководству по макету не будет. Если вы хотите, чтобы красный вид и зеленый вид сразу привязывались к их новым позициям перед анимацией вращения, анимируйте, чтобы заполнить их желаемые позиции по мере того, как контроллер просмотра анимации, тогда вы можете сделать что-то вроде этого:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

    let willBePortrait = size.width < size.height

    self.bLayoutHeightConstraint.constant = 0
    self.lLayoutWidthConstraint.constant = 0

    if willBePortrait {
        self.stackView.axis = .horizontal
        NSLayoutConstraint.deactivate(self.leftConstraints)
        NSLayoutConstraint.activate(self.bottomConstraints)
    } else {
        self.stackView.axis = .vertical
        NSLayoutConstraint.deactivate(self.bottomConstraints)
        NSLayoutConstraint.activate(self.leftConstraints)
    }
    self.view.layoutIfNeeded()

    coordinator.animate(alongsideTransition: { context in
        let halfDuration = context.transitionDuration / 2
        UIView.animate(withDuration: halfDuration, delay: halfDuration, animations: {
            if willBePortrait {
                self.bLayoutHeightConstraint.constant = sizeConstant
            } else {
                self.lLayoutWidthConstraint.constant = sizeConstant
            }
            self.view.layoutIfNeeded()
        })
    })

    super.viewWillTransition(to: size, with: coordinator)
}

РЕДАКТИРОВАТЬ: Если вы хотите анимировать красный вид во время перехода, а затем оживить красный вид назад в конце, то это хорошее время для использования координатора, поскольку анимация может произойти в блоке анимации, и вы можете разделить внешние и внутренние внутри него:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

    let willBePortrait = size.width < size.height

    if willBePortrait {
        self.stackView.axis = .horizontal
        NSLayoutConstraint.deactivate(self.leftConstraints)
        NSLayoutConstraint.activate(self.bottomConstraints)
    } else {
        self.stackView.axis = .vertical
        NSLayoutConstraint.deactivate(self.bottomConstraints)
        NSLayoutConstraint.activate(self.leftConstraints)
    }
    self.view.layoutIfNeeded()

    coordinator.animate(alongsideTransition: { context in
        let halfDuration = context.transitionDuration / 2

        UIView.animate(withDuration: halfDuration, animations: {
            if willBePortrait {
                self.lLayoutWidthConstraint.constant = 0
            } else {
                self.bLayoutHeightConstraint.constant = 0
            }
        })
        UIView.animate(withDuration: halfDuration, delay: halfDuration, animations: {
            if willBePortrait {
                self.bLayoutHeightConstraint.constant = sizeConstant
            } else {
                self.lLayoutWidthConstraint.constant = sizeConstant
            }
            self.view.layoutIfNeeded()
        })

    })
    super.viewWillTransition(to: size, with: coordinator)
}