I'm trying to get my views to animate from top to bottom. Currently, when changing the text of my label, between nil and some "error message", the labels are animated from the center of its intrinsic size, but I want the regular "label" to be "static" and only animate the errorlabel. Basically the error label should be located directly below the regular label and the errorlabel should be expanded according to its (intrinsic)height. This is essentially for a checkbox. I want to show the error message when the user hasn't checked the checkbox yet, but are trying to proceed further. The code is just a basic implementation that explains the problem. I've tried adjusting anchorPoint and contentMode for the containerview but those doesn't seem to work the way I thought. Sorry if the indentation is weird
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.
}
}
So, since it's a control that I wanted to create (checkbox) in this case with an error message, I manipulated the frames directly, based on the bounds. So to get it to work properly, I used a combination of overriding intrinsicContentSize and layoutSubviews and some minor extra stuff. The class contains a bit more than provided, but the provided code should hopefully explain the approach I went with.
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
}
}