Swift如何封装可复用的自定义浮动标签UITextField
可复用浮动标签UITextField封装实现
原有代码迁移失效的核心原因是逻辑强依赖ViewController内通过IBOutlet关联的标签、约束属性,没有将相关UI元素和交互逻辑收口到自定义输入框内部。封装时需要把浮动标签、布局计算、状态监听、动画逻辑全部内置到UITextField子类中,对外仅暴露必要的样式配置接口即可。
完整组件代码
import UIKit class FloatingLabelTextField: UITextField { // MARK: 可配置样式属性 /// 未浮动状态下提示文字字体 @IBInspectable var placeholderFont: UIFont = .systemFont(ofSize: 14) { didSet { updateStatus() } } /// 浮动状态下标签字体 @IBInspectable var floatingLabelFont: UIFont = .systemFont(ofSize: 10) { didSet { updateStatus() } } /// 提示文字内容 @IBInspectable var placeholderText: String = "" { didSet { placeholderLabel.text = placeholderText // 清空系统默认占位符,避免重复显示 attributedPlaceholder = NSAttributedString(string: "", attributes: [:]) } } /// 未浮动状态提示文字颜色 @IBInspectable var placeholderColor: UIColor = .lightGray { didSet { updateStatus() } } /// 浮动状态标签颜色 @IBInspectable var floatingLabelColor: UIColor = .systemBlue { didSet { updateStatus() } } /// 输入内容内边距 private let contentPadding = UIEdgeInsets(top: 14, left: 12, bottom: 6, right: 12) /// 内置浮动提示标签 private lazy var placeholderLabel: UILabel = { let label = UILabel() label.font = placeholderFont label.textColor = placeholderColor label.translatesAutoresizingMaskIntoConstraints = false return label }() /// 标签位置约束,动画时需要动态修改 private var labelTopConstraint: NSLayoutConstraint! private var labelLeadingConstraint: NSLayoutConstraint! // MARK: 初始化 override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() } private func setupUI() { borderStyle = .roundedRect addSubview(placeholderLabel) // 初始化标签约束 labelTopConstraint = placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: contentPadding.top) labelLeadingConstraint = placeholderLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: contentPadding.left) NSLayoutConstraint.activate([labelTopConstraint, labelLeadingConstraint]) // 监听编辑状态,无需外部设置delegate addTarget(self, action: #selector(handleEditingBegin), for: .editingDidBegin) addTarget(self, action: #selector(handleEditingEnd), for: .editingDidEnd) addTarget(self, action: #selector(handleTextChange), for: .editingChanged) } // MARK: 编辑状态处理 @objc private func handleEditingBegin() { animateLabelToFloatingPosition(animated: true) } @objc private func handleEditingEnd() { // 无输入内容时标签回到原位,有内容时保持浮动 guard text?.isEmpty ?? true else { return } animateLabelToNormalPosition(animated: true) } @objc private func handleTextChange() { // 兼容外部代码直接修改text属性的场景,避免状态错位 if (text?.isEmpty ?? true) && !isFirstResponder { animateLabelToNormalPosition(animated: false) } else { animateLabelToFloatingPosition(animated: false) } } // MARK: 动画逻辑 private func animateLabelToFloatingPosition(animated: Bool) { let update = { self.placeholderLabel.font = self.floatingLabelFont self.placeholderLabel.textColor = self.floatingLabelColor self.labelTopConstraint.constant = 4 self.labelLeadingConstraint.constant = 8 self.layoutIfNeeded() } animated ? UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: update) : update() } private func animateLabelToNormalPosition(animated: Bool) { let update = { self.placeholderLabel.font = self.placeholderFont self.placeholderLabel.textColor = self.placeholderColor self.labelTopConstraint.constant = self.contentPadding.top self.labelLeadingConstraint.constant = self.contentPadding.left self.layoutIfNeeded() } animated ? UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: update) : update() } private func updateStatus() { if (text?.isEmpty ?? true) && !isFirstResponder { placeholderLabel.font = placeholderFont placeholderLabel.textColor = placeholderColor } else { placeholderLabel.font = floatingLabelFont placeholderLabel.textColor = floatingLabelColor } } // MARK: 重写边距计算,替换原有空leftView/rightView的实现 override func textRect(forBounds bounds: CGRect) -> CGRect { return bounds.inset(by: contentPadding) } override func editingRect(forBounds bounds: CGRect) -> CGRect { return bounds.inset(by: contentPadding) } override func rightViewRect(forBounds bounds: CGRect) -> CGRect { var rect = super.rightViewRect(forBounds: bounds) rect.origin.x -= contentPadding.right return rect } override func leftViewRect(forBounds bounds: CGRect) -> CGRect { var rect = super.leftViewRect(forBounds: bounds) rect.origin.x += contentPadding.left return rect } }
调用方式
纯代码调用
let inputField = FloatingLabelTextField(frame: CGRect(x: 20, y: 100, width: view.bounds.width - 40, height: 50)) inputField.placeholderText = "请输入手机号" inputField.placeholderColor = .lightGray inputField.floatingLabelColor = .systemBlue view.addSubview(inputField)
Storyboard/Xib调用
- 拖入普通UITextField控件,将Class属性修改为
FloatingLabelTextField - 在属性检查器面板可直接可视化配置提示文字、颜色、字体等属性,无需额外关联IBOutlet或添加约束
效果示意
实现说明
- 组件内部自行监听编辑事件,不强制占用delegate属性,不会和外部业务的delegate调用冲突
- 补全了原有示例的逻辑缺失:输入内容后失去焦点标签保持浮动状态,清空内容后自动回归原位
- 通过重写系统边距方法实现内边距,无需添加空占位View,性能更好
- 支持
@IBInspectable可视化配置,同时兼容纯代码、Interface Builder两种使用场景 - 兼容外部代码直接修改
text属性的场景,不会出现标签状态和输入内容不匹配的问题
内容的提问来源于stack exchange,提问作者Halil Emin Çalışkan
相关产品推荐
相关产品推荐

