[英]Swift: Push Objects under the Label according to the number of lines
I'm trying to make the same behavior of the Material design textfield with a custom textfield.我正在尝试使用自定义文本字段使 Material design 文本字段具有相同的行为。 I created a class that inherits from textfield and every thing is working fine.
我创建了一个继承自 textfield 的类,并且一切正常。 The only problem is in one scenario.
唯一的问题是在一种情况下。 when I have an object under the textfield, and i add the error label under the text field.
当我在文本字段下有一个对象时,我在文本字段下添加了错误标签。 the error label might be more than one line.
错误标签可能不止一行。 so it overlays the object under the textfield.
所以它覆盖了文本字段下的对象。 However, in the material design library, the objects under the textfield are automatically pushed down according ton the number of lines of the error label.
但是在 Material Design 库中,textfield 下的对象会根据错误标签的行数自动下推。
here is my custom textfield code:这是我的自定义文本字段代码:
import UIKit
import RxSwift
import RxCocoa
class FloatingTextField2: UITextField {
var placeholderLabel: UILabel!
var line: UIView!
var errorLabel: UILabel!
let bag = DisposeBag()
var activeColor = Constants.colorBlue
var inActiveColor = UIColor(red: 84/255.0, green: 110/255.0, blue: 122/255.0, alpha: 0.8)
var errorColorFull = UIColor(red: 254/255.0, green: 103/255.0, blue: 103/255.0, alpha: 1.0)
//var errorColorParcial = UIColor(red: 254/255.0, green: 103/255.0, blue: 103/255.0, alpha: 0.5)
private var lineYPosition: CGFloat!
private var lineXPosition: CGFloat!
private var lineWidth: CGFloat!
private var lineHeight: CGFloat!
private var errorLabelYPosition: CGFloat!
private var errorLabelXPosition: CGFloat!
private var errorLabelWidth: CGFloat!
private var errorLabelHeight: CGFloat!
var maxFontSize: CGFloat = 14
var minFontSize: CGFloat = 11
let errorLabelFont = UIFont(name: "Lato-Regular", size: 12)
var animationDuration = 0.35
var placeholderText: String = "" {
didSet {
if placeholderLabel != nil {
placeholderLabel.text = placeholderText
}
}
}
var isTextEntrySecured: Bool = false {
didSet {
self.isSecureTextEntry = isTextEntrySecured
}
}
override func draw(_ rect: CGRect) {
//setUpUI()
}
override func awakeFromNib() {
setUpUI()
}
func setUpUI() {
if placeholderLabel == nil {
placeholderLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.frame.width, height: 20))
self.addSubview(placeholderLabel)
self.borderStyle = .none
placeholderLabel.text = "Placeholder Preview"
placeholderLabel.textColor = inActiveColor
self.font = UIFont(name: "Lato-Regular", size: maxFontSize)
self.placeholderLabel.font = UIFont(name: "Lato-Regular", size: maxFontSize)
self.placeholder = ""
self.textColor = .black
setUpTextField()
}
if line == nil {
lineYPosition = self.frame.height
lineXPosition = -16
lineWidth = self.frame.width + 32
lineHeight = 1
line = UIView(frame: CGRect(x: lineXPosition, y: lineYPosition, width: lineWidth, height: lineHeight))
self.addSubview(line)
line.backgroundColor = inActiveColor
}
if errorLabel == nil {
errorLabelYPosition = lineYPosition + 8
errorLabelXPosition = 0
errorLabelWidth = self.frame.width
errorLabelHeight = calculateErrorLabelHeight(text: "")
errorLabel = UILabel(frame: CGRect(x: 0, y: errorLabelYPosition, width: errorLabelWidth, height: errorLabelHeight))
self.addSubview(errorLabel)
errorLabel.numberOfLines = 0
errorLabel.textColor = errorColorFull
errorLabel.text = ""
errorLabel.font = errorLabelFont
sizeToFit()
}
}
func setUpTextField(){
self.rx.controlEvent(.editingDidBegin).subscribe(onNext: { (next) in
if self.text?.isEmpty ?? false {
self.animatePlaceholderUp()
}
}).disposed(by: bag)
self.rx.controlEvent(.editingDidEnd).subscribe(onNext: { (next) in
if self.text?.isEmpty ?? false {
self.animatePlaceholderCenter()
}
}).disposed(by: bag)
}
func setErrorText(_ error: String?, errorAccessibilityValue: String?) {
if let errorText = error {
self.resignFirstResponder()
errorLabelHeight = calculateErrorLabelHeight(text: errorText)
self.errorLabel.frame = CGRect(x: 0, y: errorLabelYPosition, width: errorLabelWidth, height: errorLabelHeight)
self.errorLabel.text = errorText
self.errorLabel.isHidden = false
self.line.backgroundColor = errorColorFull
}else{
self.errorLabel.text = ""
self.errorLabel.isHidden = true
}
errorLabel.accessibilityIdentifier = errorAccessibilityValue ?? "textinput_error"
}
func animatePlaceholderUp(){
UIView.animate(withDuration: animationDuration, animations: {
self.line.frame.size.height = 2
self.line.backgroundColor = self.activeColor
self.placeholderLabel.font = self.placeholderLabel.font.withSize(self.minFontSize)
self.placeholderLabel.textColor = self.activeColor
self.placeholderLabel.frame = CGRect(x: 0, y: (self.frame.height/2 + 8) * -1, width: self.frame.width, height: self.frame.height)
self.layoutIfNeeded()
}) { (done) in
}
}
func animatePlaceholderCenter(){
UIView.animate(withDuration: animationDuration, animations: {
self.line.frame.size.height = 1
self.line.backgroundColor = self.inActiveColor
self.placeholderLabel.font = self.placeholderLabel.font.withSize(self.maxFontSize)
self.placeholderLabel.textColor = self.inActiveColor
self.placeholderLabel.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
self.layoutIfNeeded()
}) { (done) in
}
}
func calculateErrorLabelHeight(text:String) -> CGFloat{
let font = errorLabelFont
let width = self.frame.width
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height
}
}
How can I solve this problem?我怎么解决这个问题? I could not find anything on stack overflow or google related to my problem.
我在堆栈溢出或谷歌上找不到与我的问题相关的任何内容。
As mentioned in the comments:正如评论中提到的:
UITextField
will show them outside the Bounds of the field, meaning they won't affect the frame (and thus the constraints)UITextField
会将它们显示在字段的边界之外,这意味着它们不会影响框架(因此不会影响约束)The key to getting your "error" label to expand the view is to apply multiple vertical constraints, and activate / deactivate as needed.获取“错误”标签以展开视图的关键是应用多个垂直约束,并根据需要激活/停用。
Here is a complete example of a custom UIView
which contains a text field, a placeholder label and an error label.这是自定义
UIView
的完整示例,其中包含文本字段、占位符标签和错误标签。 The example view controller includes "demo" buttons to show the capabilities.示例视图控制器包括“演示”按钮以显示功能。
I suggest you add this code and try it out.我建议您添加此代码并尝试一下。 If it suits your needs, there are plenty of comments in it that you should be able to tweak fonts, spacing, etc to your liking.
如果它适合您的需求,其中有很多评论,您应该能够根据自己的喜好调整字体、间距等。
Or, it should at least give you some ideas of how to set up your own.或者,它至少应该给你一些关于如何设置你自己的想法。
FloatingTextFieldView - UIView subclass FloatingTextFieldView - UIView 子类
class FloatingTextFieldView: UIView, UITextFieldDelegate {
var placeHolderTopConstraint: NSLayoutConstraint!
var placeHolderCenterYConstraint: NSLayoutConstraint!
var placeHolderLeadingConstraint: NSLayoutConstraint!
var lineHeightConstraint: NSLayoutConstraint!
var errorLabelBottomConstraint: NSLayoutConstraint!
var activeColor: UIColor = UIColor.blue
var inActiveColor: UIColor = UIColor(red: 84/255.0, green: 110/255.0, blue: 122/255.0, alpha: 0.8)
var errorColorFull: UIColor = UIColor(red: 254/255.0, green: 103/255.0, blue: 103/255.0, alpha: 1.0)
var animationDuration = 0.35
var maxFontSize: CGFloat = 14
var minFontSize: CGFloat = 11
let errorLabelFont = UIFont(name: "Lato-Regular", size: 12)
let placeholderLabel: UILabel = {
let v = UILabel()
v.text = "Default Placeholder"
v.setContentHuggingPriority(.required, for: .vertical)
return v
}()
let line: UIView = {
let v = UIView()
v.backgroundColor = .lightGray
return v
}()
let errorLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.text = "Default Error"
v.setContentCompressionResistancePriority(.required, for: .vertical)
return v
}()
let textField: UITextField = {
let v = UITextField()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
clipsToBounds = true
backgroundColor = .white
[textField, line, placeholderLabel, errorLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
// place holder label gets 2 vertical constraints
// top of view
// centerY to text field
placeHolderTopConstraint = placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0.0)
placeHolderCenterYConstraint = placeholderLabel.centerYAnchor.constraint(equalTo: textField.centerYAnchor, constant: 0.0)
// place holder leading constraint is 16-pts (when centered on text field)
// when animated above text field, we'll change the constant to 0
placeHolderLeadingConstraint = placeholderLabel.leadingAnchor.constraint(equalTo: textField.leadingAnchor, constant: 16.0)
// error label bottom constrained to bottom of view
// will be activated when shown, deactivated when hidden
errorLabelBottomConstraint = errorLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
// line height constraint constant changes between 1 and 2 (inactive / active)
lineHeightConstraint = line.heightAnchor.constraint(equalToConstant: 1.0)
NSLayoutConstraint.activate([
// text field top 16-pts from top of view
// leading and trailing = 0
textField.topAnchor.constraint(equalTo: topAnchor, constant: 16.0),
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// text field height = 24
textField.heightAnchor.constraint(equalToConstant: 24.0),
// text field bottom is AT LEAST 4 pts
textField.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -4.0),
// line view top is 2-pts below text field bottom
// leading and trailing = 0
line.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 2.0),
line.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
line.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// error label top is 4-pts from text field bottom
// leading and trailing = 0
errorLabel.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 4.0),
errorLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
errorLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
placeHolderCenterYConstraint,
placeHolderLeadingConstraint,
lineHeightConstraint,
])
// I'm not using Rx, so set the delegate
textField.delegate = self
textField.font = UIFont(name: "Lato-Regular", size: maxFontSize)
textField.textColor = .black
placeholderLabel.font = UIFont(name: "Lato-Regular", size: maxFontSize)
placeholderLabel.textColor = inActiveColor
line.backgroundColor = inActiveColor
errorLabel.textColor = errorColorFull
errorLabel.font = errorLabelFont
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField.text?.isEmpty ?? false {
self.animatePlaceholderUp()
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
if textField.text?.isEmpty ?? false {
self.animatePlaceholderCenter()
}
}
func animatePlaceholderUp() -> Void {
UIView.animate(withDuration: animationDuration, animations: {
// increase line height
self.lineHeightConstraint.constant = 2.0
// set line to activeColor
self.line.backgroundColor = self.activeColor
// set placeholder label font and color
self.placeholderLabel.font = self.placeholderLabel.font.withSize(self.minFontSize)
self.placeholderLabel.textColor = self.activeColor
// deactivate placeholder label CenterY constraint
self.placeHolderCenterYConstraint.isActive = false
// activate placeholder label Top constraint
self.placeHolderTopConstraint.isActive = true
// move placeholder label leading to 0
self.placeHolderLeadingConstraint.constant = 0
self.layoutIfNeeded()
}) { (done) in
}
}
func animatePlaceholderCenter() -> Void {
UIView.animate(withDuration: animationDuration, animations: {
// decrease line height
self.lineHeightConstraint.constant = 1.0
// set line to inactiveColor
self.line.backgroundColor = self.inActiveColor
// set placeholder label font and color
self.placeholderLabel.font = self.placeholderLabel.font.withSize(self.maxFontSize)
self.placeholderLabel.textColor = self.inActiveColor
// deactivate placeholder label Top constraint
self.placeHolderTopConstraint.isActive = false
// activate placeholder label CenterY constraint
self.placeHolderCenterYConstraint.isActive = true
// move placeholder label leading to 16
self.placeHolderLeadingConstraint.constant = 16
self.layoutIfNeeded()
}) { (done) in
}
}
func setErrorText(_ error: String?, errorAccessibilityValue: String?, endEditing: Bool) {
if let errorText = error {
UIView.animate(withDuration: 0.05, animations: {
self.errorLabel.text = errorText
self.line.backgroundColor = self.errorColorFull
self.errorLabel.isHidden = false
// activate error label Bottom constraint
self.errorLabelBottomConstraint.isActive = true
}) { (done) in
if endEditing {
self.textField.resignFirstResponder()
}
}
}else{
UIView.animate(withDuration: 0.05, animations: {
self.errorLabel.text = ""
self.line.backgroundColor = self.inActiveColor
self.errorLabel.isHidden = true
// deactivate error label Bottom constraint
self.errorLabelBottomConstraint.isActive = false
}) { (done) in
if endEditing {
self.textField.resignFirstResponder()
}
}
}
errorLabel.accessibilityIdentifier = errorAccessibilityValue ?? "textinput_error"
}
// func to set / clear element background colors
// to make it easy to see the frames
func showHideFrames(show b: Bool) -> Void {
if b {
self.backgroundColor = UIColor(red: 0.8, green: 0.8, blue: 1.0, alpha: 1.0)
placeholderLabel.backgroundColor = .cyan
errorLabel.backgroundColor = .green
textField.backgroundColor = .yellow
} else {
self.backgroundColor = .white
[placeholderLabel, errorLabel, textField].forEach {
$0.backgroundColor = .clear
}
}
}
}
DemoFLoatingTextViewController DemoFLoatingTextViewController
class DemoFLoatingTextViewController: UIViewController {
// FloatingTextFieldView
let sampleFTF: FloatingTextFieldView = {
let v = FloatingTextFieldView()
return v
}()
// a label to constrain below the FloatingTextFieldView
// so we can see it gets "pushed down"
let demoLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.text = "This is a label outside the Floating Text Field. As you will see, it gets \"pushed down\" when the error label is shown."
v.backgroundColor = .brown
v.textColor = .yellow
return v
}()
// buttons to Demo the functionality
let btnA: UIButton = {
let b = UIButton(type: .system)
b.setTitle("End Editing", for: .normal)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return b
}()
let btnB: UIButton = {
let b = UIButton(type: .system)
b.setTitle("Set Error", for: .normal)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return b
}()
let btnC: UIButton = {
let b = UIButton(type: .system)
b.setTitle("Clear Error", for: .normal)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return b
}()
let btnD: UIButton = {
let b = UIButton(type: .system)
b.setTitle("Set & End", for: .normal)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return b
}()
let btnE: UIButton = {
let b = UIButton(type: .system)
b.setTitle("Clear & End", for: .normal)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return b
}()
let btnF: UIButton = {
let b = UIButton(type: .system)
b.setTitle("Show Frames", for: .normal)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return b
}()
let btnG: UIButton = {
let b = UIButton(type: .system)
b.setTitle("Hide Frames", for: .normal)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return b
}()
let errorMessages: [String] = [
"Simple Error",
"This will end up being a Multiline Error message. It is long enough to cause word wrapping."
]
var errorCount: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// add Demo buttons
let btnStack = UIStackView()
btnStack.axis = .vertical
btnStack.spacing = 6
btnStack.translatesAutoresizingMaskIntoConstraints = false
[[btnA], [btnB, btnC], [btnD, btnE], [btnF, btnG]].forEach { btns in
let sv = UIStackView()
sv.distribution = .fillEqually
sv.spacing = 12
sv.translatesAutoresizingMaskIntoConstraints = false
btns.forEach {
sv.addArrangedSubview($0)
}
btnStack.addArrangedSubview(sv)
}
view.addSubview(btnStack)
// add FloatingTextFieldView and demo label
view.addSubview(sampleFTF)
view.addSubview(demoLabel)
sampleFTF.translatesAutoresizingMaskIntoConstraints = false
demoLabel.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// buttons stack Top = 20, centerX, width = 80% of view width
btnStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
btnStack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
btnStack.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.8),
// FloatingTextFieldView Top = 40-pts below buttons stack
sampleFTF.topAnchor.constraint(equalTo: btnStack.bottomAnchor, constant: 40.0),
// FloatingTextFieldView Leading = 60-pts
sampleFTF.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
// FloatingTextFieldView width = 240
sampleFTF.widthAnchor.constraint(equalToConstant: 240.0),
// Note: we are not setting the FloatingTextFieldView Height!
// constrain demo label Top = 8-pts below FloatingTextFieldView bottom
demoLabel.topAnchor.constraint(equalTo: sampleFTF.bottomAnchor, constant: 8.0),
// Leading = FloatingTextFieldView Leading
demoLabel.leadingAnchor.constraint(equalTo: sampleFTF.leadingAnchor),
// Width = 200
demoLabel.widthAnchor.constraint(equalToConstant: 200.0),
])
// add touchUpInside targets for demo buttons
btnA.addTarget(self, action: #selector(endEditing(_:)), for: .touchUpInside)
btnB.addTarget(self, action: #selector(setError(_:)), for: .touchUpInside)
btnC.addTarget(self, action: #selector(clearError(_:)), for: .touchUpInside)
btnD.addTarget(self, action: #selector(setAndEnd(_:)), for: .touchUpInside)
btnE.addTarget(self, action: #selector(clearAndEnd(_:)), for: .touchUpInside)
btnF.addTarget(self, action: #selector(showFrames(_:)), for: .touchUpInside)
btnG.addTarget(self, action: #selector(hideFrames(_:)), for: .touchUpInside)
}
@objc func endEditing(_ sender: Any) -> Void {
sampleFTF.textField.resignFirstResponder()
}
@objc func setError(_ sender: Any) -> Void {
sampleFTF.setErrorText(errorMessages[errorCount % 2], errorAccessibilityValue: "", endEditing: false)
errorCount += 1
}
@objc func clearError(_ sender: Any) -> Void {
sampleFTF.setErrorText(nil, errorAccessibilityValue: "", endEditing: false)
}
@objc func setAndEnd(_ sender: Any) -> Void {
sampleFTF.setErrorText(errorMessages[errorCount % 2], errorAccessibilityValue: "", endEditing: true)
errorCount += 1
}
@objc func clearAndEnd(_ sender: Any) -> Void {
sampleFTF.setErrorText(nil, errorAccessibilityValue: "", endEditing: true)
}
@objc func showFrames(_ sender: Any) -> Void {
sampleFTF.showHideFrames(show: true)
}
@objc func hideFrames(_ sender: Any) -> Void {
sampleFTF.showHideFrames(show: false)
}
}
Example results:结果示例:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.