You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 14:48:15