简体   繁体   English

Swift:根据行数推送Label下的Objects

[英]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:正如评论中提到的:

  • You'll be much better off using constraints rather than explicit frames使用约束而不是显式框架会好得多
  • Adding subviews to a UITextField will show them outside the Bounds of the field, meaning they won't affect the frame (and thus the constraints)将子视图添加到UITextField会将它们显示在字段的边界之外,这意味着它们不会影响框架(因此不会影响约束)
  • If the constraints are set properly, they will control the "containing view" height如果约束设置正确,它们将控制“包含视图”的高度

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM