Как анимировать высоту вида входного аксессуара?

Я испытываю странное поведение при анимации высоты вспомогательного представления ввода. Что я делаю не так?

Я создаю подкласс UIInputView (InputView) с одним подпредставлением. Высота InputView и его intrinsicContentSize контролируется подпредставлением. InputView имеет высоту 50 пикселей, когда isVisible равно true, и 0 пикселей, если isVisible равно false.

import UIKit

class InputView: UIInputView {
    private let someHeight: CGFloat = 50.0, zeroHeight: CGFloat = 0.0
    private let subView = UIView()
    private var hide: NSLayoutConstraint?, show: NSLayoutConstraint?

    var isVisible: Bool {
        get {
            return show!.isActive
        }
        set {
            // Always deactivate constraints before activating conflicting ones
            if newValue == true {
                hide?.isActive = false
                show?.isActive = true
            } else {
                show?.isActive = false
                hide?.isActive = true
            }
        }
    }

    // MARK: Sizing

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        return CGSize(width: size.width, height: someHeight)
    }

    override var intrinsicContentSize: CGSize {
        return CGSize.init(width: bounds.size.width, height: subView.bounds.size.height)
    }

    // MARK: Initializers

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(frame: CGRect, inputViewStyle: UIInputViewStyle) {
        super.init(frame: frame, inputViewStyle: inputViewStyle)

        addSubview(subView)
        subView.backgroundColor = UIColor.purple

        translatesAutoresizingMaskIntoConstraints = false
        subView.translatesAutoresizingMaskIntoConstraints = false

        subView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
        subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true

        show = subView.heightAnchor.constraint(equalToConstant: someHeight)
        hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
        hide?.isActive = true
    }
}

Контроллер основного представления переключает isVisible в односекундном блоке анимации при нажатии кнопки.

import UIKit

class MainViewController: UIViewController {
    let testInputView = InputView.init(frame: .zero, inputViewStyle: .default)

    @IBAction func button(_ sender: AnyObject) {
        UIView.animate(withDuration: 1.0) {
            let isVisible = self.testInputView.isVisible
            self.testInputView.isVisible = !isVisible
            self.testInputView.layoutIfNeeded()
        }
    }

    override var canBecomeFirstResponder: Bool {
        return true
    }

    override var inputAccessoryView: UIView? {
        return testInputView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Я ожидаю, что входное вспомогательное представление будет плавно увеличиваться от нижней части экрана, когда isVisible установлено на true, и плавно сжиматься до кнопки экрана, когда isVisible установлено на false. Вместо этого фоновое наложение клавиатуры отображается на полной высоте 50 пикселей, как только isVisible становится true, а вид вспомогательного ввода увеличивается из центра его фрейма.

Фиолетовое поле не растет из нижней части экрана и окружено фоном наложения клавиатуры

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

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


person Alan Kantz    schedule 06.11.2017    source источник


Ответы (1)


Это даст вам правильную анимацию:

    UIView.animate(withDuration: 1.0) {
        let isVisible = self.testInputView.isVisible
        self.testInputView.isVisible = !isVisible
        self.testInputView.superview?.superview?.layoutIfNeeded()
    }

Однако никогда не рекомендуется вызывать superview, если Apple меняет дизайн. Так что может быть лучший ответ.

Вот что представляют супервизоры:

print(testInputView.superview) // UIInputSetHostView

print(testInputView.superview?.superview) // UIInputSetContainerView

РЕДАКТИРОВАНИЕ: ДОБАВЛЕНО БЕЗОПАСНОЕ РЕШЕНИЕ

Я не слишком хорошо знаком с UIInputView. Но один из способов решить эту проблему без вызова суперпредставления — анимировать только изменение высоты подпредставления:

Шаг 1.
Переместите isVisible за пределы блока анимации.

@IBAction func button(_ sender: AnyObject) {
    let isVisible = self.testInputView.isVisible
    self.testInputView.isVisible = !isVisible
    UIView.animate(withDuration: 1.0) {
        self.testInputView.layoutIfNeeded()
    }
}

Шаг 2. Создайте новый метод в InputView, который изменяет ограничение высоты InputView вместо встроенногоContentSize.

private func updateHeightConstraint(height: CGFloat) {
    for constraint in constraints {
        if constraint.firstAttribute == .height {
            constraint.constant = height
        }
    }
    self.layoutIfNeeded()
}

Шаг 3. И вызовите этот метод внутри установщика.

if newValue == true {
     updateHeightConstraint(height: someHeight)
     hide?.isActive = false
     show?.isActive = true
} else {
     updateHeightConstraint(height: zeroHeight)
     show?.isActive = false
     hide?.isActive = true
}

Шаг 4: Наконец, некоторые изменения в файле init.

override init(frame: CGRect, inputViewStyle: UIInputViewStyle) {
    super.init(frame: frame, inputViewStyle: inputViewStyle)

    addSubview(subView)

    backgroundColor = .clear
    subView.backgroundColor = UIColor.purple

    subView.translatesAutoresizingMaskIntoConstraints = false

    subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
    subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
    subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true

    show = subView.heightAnchor.constraint(equalToConstant: someHeight)
    hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
    hide?.isActive = true
}

Вывод: это приводит к тому, что InputView изменяет свою высоту перед анимацией высоты фиолетового подпредставления. Единственным недостатком является UIInputView, который по умолчанию имеет серый фон и не может быть изменен на Clear. Однако вы можете использовать тот же backgroundColor, что и для VC.

Но если вместо этого вы должны использовать обычный UIView в качестве InputAccessoryView, по умолчанию он будет UIColor.clear. Тогда первый «скачок» не будет замечен.

person alengqvist    schedule 07.11.2017
comment
Я не принимаю это как лучший ответ пока в надежде, что кто-то покажет более безопасное решение, но это работает прекрасно. Совет о том, как найти супер просмотры, также очень полезен. - person Alan Kantz; 07.11.2017
comment
К сожалению, метод ограничения высоты не анимирует меня должным образом. Лучшим решением было использование superview?.superview, хотя в iOS 14 это может вызвать бесконечный цикл при вызове во время анимации клавиатуры. В этом случае работает переключение на superview?.superview?.superview, которое является верхним представлением в иерархии. - person Daniel; 01.10.2020