Анимация сверху, а не по центру внутреннего размера

Я пытаюсь заставить свои взгляды анимироваться сверху вниз. В настоящее время при изменении текста моей метки между nil и некоторым «сообщением об ошибке» метки анимируются из центра своего внутреннего размера, но я хочу, чтобы обычная «метка» была «статической» и анимировала только метку ошибки. По сути, метка ошибки должна быть расположена непосредственно под обычной меткой, а метка ошибки должна быть расширена в соответствии с ее (внутренней) высотой. Это по существу для флажка. Я хочу показать сообщение об ошибке, когда пользователь еще не установил флажок, но пытается продолжить. Код — это просто базовая реализация, объясняющая проблему. Я попытался настроить anchorPoint и contentMode для представления контейнера, но, похоже, они не работают так, как я думал. Извините, если отступ странный

import UIKit
class ViewController: UIViewController {

    let container = UIView()
    let errorLabel = UILabel()


    var bottomLabel: NSLayoutConstraint!
    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(container)
        container.contentMode = .top
        container.translatesAutoresizingMaskIntoConstraints = false
        container.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        container.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor).isActive = true
        container.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        container.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true

        let label = UILabel()
        label.text = "Very long text that i would like to show to full extent and eventually add an error message to. It'll work on multiple rows obviously"
        label.numberOfLines = 0
        container.contentMode = .top
        container.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
        label.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true

        container.addSubview(errorLabel)
        errorLabel.setContentHuggingPriority(UILayoutPriority(300), for: .vertical)
        errorLabel.translatesAutoresizingMaskIntoConstraints = false
        errorLabel.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
        errorLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
        errorLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true

        bottomLabel = errorLabel.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor)
        bottomLabel.isActive = false

        errorLabel.numberOfLines = 0

        container.backgroundColor = .green
        let tapRecognizer = UITapGestureRecognizer()
        tapRecognizer.addTarget(self, action: #selector(onTap))
        container.addGestureRecognizer(tapRecognizer)
    }

    @objc func onTap() {

        self.container.layoutIfNeeded()
        UIView.animate(withDuration: 0.3, animations: {
            let active = !self.bottomLabel.isActive
            self.bottomLabel.isActive = active
            self.errorLabel.text = active ? "A veru very veru very veru very veru very veru very veru very veru very veru very long Error message" : nil


            self.container.layoutIfNeeded()
        })
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

person Kaz    schedule 12.02.2018    source источник
comment
Вы хотите, чтобы ваша метка сообщения об ошибке скользила вниз в поле зрения? Или вы хотите, чтобы анимация выглядела так, как будто метка ошибки уже есть, но открывается при скольжении крышки?   -  person DonMag    schedule 12.02.2018
comment
Я думаю, что это второй вариант, о котором вы говорите, который мне нужен. Я хочу, чтобы верхняя часть метки ошибок была прикреплена к нижней части этикетки (как сейчас), а также увеличивала ее внутреннюю высоту и раскрывалась, как вы говорите. Думайте об этом как о форме, в которой я хочу отображать сообщение об ошибке, если ввод неправильно сформирован. Это также можно было бы сказать, представление ввода, где я хочу отобразить ошибку ниже, если ввод неверен.   -  person Kaz    schedule 13.02.2018


Ответы (2)


Мне было немного сложно заставить динамические многострочные метки «анимироваться» так, как я хочу, особенно когда я хочу «скрыть» метку.

Один подход: создайте 2 ярлыка «ошибка», один из которых накладывается поверх другого. Используйте «скрытую» метку для управления ограничениями представления контейнера. При анимации изменения границы представления контейнера будут эффективно «показывать» и «скрывать» (показывать/скрывать) «видимую» метку.

Вот пример, который вы можете запустить прямо на странице Playground:

import UIKit
import PlaygroundSupport

class RevealViewController: UIViewController {

    let container = UIView()
    let staticLabel = UILabel()
    let hiddenErrorLabel = UILabel()
    let visibleErrorLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()

        // colors, just so we can see the bounds of the labels
        view.backgroundColor = .lightGray
        container.backgroundColor = .green
        staticLabel.backgroundColor = .yellow
        visibleErrorLabel.backgroundColor = .cyan

        // we don't want to see this label, so set its alpha to zero
        hiddenErrorLabel.alpha = 0.0

        // we want the Error Label to be "revealed" - so when it is has text it is initially "covered"
        container.clipsToBounds = true

        // all labels may be multiple lines
        staticLabel.numberOfLines = 0
        hiddenErrorLabel.numberOfLines = 0
        visibleErrorLabel.numberOfLines = 0

        // initial text in the "static" label
        staticLabel.text = "Very long text that i would like to show to full extent and eventually add an error message to. It'll work on multiple rows obviously"

        // add the container view to the VC's view
        // pin it to the sides, and 100-pts from the top
        // NO bottom constraint
        view.addSubview(container)
        container.translatesAutoresizingMaskIntoConstraints = false
        container.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0).isActive = true
        container.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        container.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true

        // add the static label to the container
        // pin it to the top and sides
        // NO bottom constraint
        container.addSubview(staticLabel)
        staticLabel.translatesAutoresizingMaskIntoConstraints = false
        staticLabel.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
        staticLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
        staticLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true

        // add the "hidden" error label to the container
        // pin it to the sides, and  pin its top to the bottom of the static label
        // NO bottom constraint
        container.addSubview(hiddenErrorLabel)
        hiddenErrorLabel.translatesAutoresizingMaskIntoConstraints = false
        hiddenErrorLabel.topAnchor.constraint(equalTo: staticLabel.bottomAnchor).isActive = true
        hiddenErrorLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
        hiddenErrorLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true

        // add the "visible" error label to the container
        // pin its top, leading and trailing constraints to the hidden label
        container.addSubview(visibleErrorLabel)
        visibleErrorLabel.translatesAutoresizingMaskIntoConstraints = false
        visibleErrorLabel.topAnchor.constraint(equalTo: hiddenErrorLabel.topAnchor).isActive = true
        visibleErrorLabel.leadingAnchor.constraint(equalTo: hiddenErrorLabel.leadingAnchor).isActive = true
        visibleErrorLabel.trailingAnchor.constraint(equalTo: hiddenErrorLabel.trailingAnchor).isActive = true

        // pin the bottom of the hidden label ot the bottom of the container
        // now, when we change the text of the hidden label, it will
        // "push down / pull up" the bottom of the container view
        hiddenErrorLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true

        // add a tap gesture
        let tapRecognizer = UITapGestureRecognizer()
        tapRecognizer.addTarget(self, action: #selector(onTap))
        container.addGestureRecognizer(tapRecognizer)

    }

    var myActive = false

    @objc func onTap() {

        let errorText = "A veru very veru very veru very veru very veru very veru very veru very veru very long Error message"

        self.myActive = !self.myActive

        if self.myActive {

            // we want to SHOW the error message

            // set the error message in the VISIBLE error label
            self.visibleErrorLabel.text = errorText

            // "animate" it, with duration of 0.0 - so it is filled instantly
            // it will extend below the bottom of the container view, but won't be
            // visible yet because we set .clipsToBounds = true on the container
            UIView.animate(withDuration: 0.0, animations: {

            }, completion: {
                _ in

                // now, set the error message in the HIDDEN error label
                self.hiddenErrorLabel.text = errorText

                // the hidden label will now "push down" the bottom of the container view
                // so we can animate the "reveal"
                UIView.animate(withDuration: 0.3, animations: {
                    self.view.layoutIfNeeded()
                })

            })

        } else {

            // we want to HIDE the error message

            // clear the text from the HIDDEN error label
            self.hiddenErrorLabel.text = ""

            // the hidden label will now "pull up" the bottom of the container view
            // so we can animate the "conceal"
            UIView.animate(withDuration: 0.3, animations: {
                self.view.layoutIfNeeded()
            }, completion: {
                _ in

                // after its hidden, clear the text of the VISIBLE error label
                self.visibleErrorLabel.text = ""

            })

        }

    }

}

let vc = RevealViewController()
PlaygroundPage.current.liveView = vc
person DonMag    schedule 13.02.2018
comment
Спасибо, я раньше не тестировал игровую площадку и не знаю, как она работает. Однако я проверил ваш подход, но не заставил его работать должным образом. Как я и надеялся, он сделал раскладывающуюся анимацию, но кадр (контейнера) по-прежнему совершал скачок при анимации, из-за чего мой вид размещался над содержимым, которое было выше моего представления контейнера. Также казалось неправильным иметь две точки зрения, когда должна быть потребность только в одной. Я придумал другое решение, хотя - person Kaz; 15.02.2018

Итак, поскольку в данном случае я хотел создать элемент управления (флажок) с сообщением об ошибке, я манипулировал кадрами напрямую, основываясь на границах. Поэтому, чтобы заставить его работать правильно, я использовал комбинацию переопределения innerContentSize и layoutSubviews, а также некоторые дополнительные мелочи. Класс содержит немного больше, чем предоставлено, но предоставленный код, надеюсь, должен объяснить подход, который я использовал.

open class Checkbox: UIView {



let imageView = UIImageView()
let textView = ThemeableTapLabel()
private let errorLabel = UILabel()
var errorVisible: Bool = false
let checkboxPad: CGFloat = 8

override open var bounds: CGRect {
    didSet {
        // fixes layout when bounds change
        invalidateIntrinsicContentSize()
    }
}


open var errorMessage: String? {
    didSet {
        self.errorVisible = self.errorMessage != nil
        UIView.animate(withDuration: 0.3, animations: {
            if self.errorMessage != nil {
                self.errorLabel.text = self.errorMessage
            }
            self.setNeedsLayout()
            self.invalidateIntrinsicContentSize()
            self.layoutIfNeeded()
        }, completion: { success in

            if self.errorMessage == nil {
                self.errorLabel.text = nil
            }
        })
    }
}


func checkboxSize() -> CGSize {
    return CGSize(width: imageView.image?.size.width ?? 0, height: imageView.image?.size.height ?? 0)
}


override open func layoutSubviews() {
    super.layoutSubviews()

    frame = bounds
    let imageFrame = CGRect(x: 0, y: 0, width: checkboxSize().width, height: checkboxSize().height)
    imageView.frame = imageFrame

    let textRect = textView.textRect(forBounds: CGRect(x: (imageFrame.width + checkboxPad), y: 0, width: bounds.width - (imageFrame.width + checkboxPad), height: 10000), limitedToNumberOfLines: textView.numberOfLines)
    textView.frame = textRect


    let largestHeight = max(checkboxSize().height, textRect.height)
    let rect = errorLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: bounds.width, height: 10000), limitedToNumberOfLines: errorLabel.numberOfLines)
    //po bourect = rect.offsetBy(dx: 0, dy: imageFrame.maxY)
    let errorHeight = errorVisible ? rect.height : 0
    errorLabel.frame = CGRect(x: 0, y: largestHeight, width: bounds.width, height: errorHeight)

}

override open var intrinsicContentSize: CGSize {
    get {

        let textRect = textView.textRect(forBounds: CGRect(x: (checkboxSize().width + checkboxPad), y: 0, width: bounds.width - (checkboxSize().width + checkboxPad), height: 10000), limitedToNumberOfLines: textView.numberOfLines)

        let rect = errorLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: bounds.width, height: 10000), limitedToNumberOfLines: errorLabel.numberOfLines)
        let errorHeight = errorVisible ? rect.height : 0
        let largestHeight = max(checkboxSize().height, textRect.height)
        return CGSize(width: checkboxSize().width + 200, height: largestHeight + errorHeight)
    }
}


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

func setup() {

//...
    addSubview(imageView)
    imageView.translatesAutoresizingMaskIntoConstraints = false

    addSubview(textView)
    textView.translatesAutoresizingMaskIntoConstraints = false
    textView.numberOfLines = 0
    contentMode = .top

    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(checkboxTap(sender:)))
    self.isUserInteractionEnabled = true
    self.addGestureRecognizer(tapGesture)

    addSubview(errorLabel)
    errorLabel.contentMode = .top
    errorLabel.textColor = .red
    errorLabel.numberOfLines = 0

}
}
person Kaz    schedule 14.02.2018