Blog スタッフブログ

iOS Swift システム開発 ひとくちコードスニペット

[Swift]フォーカス時に装飾を行うUITextFieldのサブクラス

Swift

こんにちは、株式会社MIXシステム開発担当のBloomです。

今回はフォーカスされた場合に装飾を行うUITextFieldのサブクラスを作ってみます。まずはサブクラスとして実装しましょう。

import UIKit

@IBDesignable
final class HighlightableTextField: UITextField {

    // MARK: - Inactive
    @IBInspectable var inactiveBorderColor: UIColor = .systemGray3 { didSet { updateAppearance(animated: false) } }
    @IBInspectable var inactiveBorderWidth: CGFloat = 1.0 { didSet { updateAppearance(animated: false) } }
    @IBInspectable var inactiveBackgroundColor: UIColor = .clear { didSet { updateAppearance(animated: false) } }

    // MARK: - Active
    @IBInspectable var activeBorderColor: UIColor = .systemBlue { didSet { updateAppearance(animated: false) } }
    @IBInspectable var activeBorderWidth: CGFloat = 2.0 { didSet { updateAppearance(animated: false) } }
    @IBInspectable var activeBackgroundColor: UIColor = .clear { didSet { updateAppearance(animated: false) } }

    // MARK: - Common
    @IBInspectable var cornerRadius: CGFloat = 10.0 { didSet { updateAppearance(animated: false) } }
    @IBInspectable var usesAnimation: Bool = true
    @IBInspectable var animationDuration: Double = 0.15

    /// 枠線表示制御
    @IBInspectable var showsBorder: Bool = true { didSet { updateAppearance(animated: false) } }

    // MARK: - Lifecycle
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        Task { @MainActor in
            self.commonInit()
            self.updateAppearance(animated: false)
        }
    }
    
    private func commonInit() {
        removeTarget(self, action: #selector(editingDidBegin), for: .editingDidBegin)
        removeTarget(self, action: #selector(editingDidEnd), for: .editingDidEnd)

        addTarget(self, action: #selector(editingDidBegin), for: .editingDidBegin)
        addTarget(self, action: #selector(editingDidEnd), for: .editingDidEnd)
        updateAppearance(animated: false)
    }

    override func becomeFirstResponder() -> Bool {
        let result = super.becomeFirstResponder()
        if result { updateAppearance(animated: usesAnimation) }
        return result
    }

    override func resignFirstResponder() -> Bool {
        let result = super.resignFirstResponder()
        if result { updateAppearance(animated: usesAnimation) }
        return result
    }

    @objc private func editingDidBegin() {
        updateAppearance(animated: usesAnimation)
    }

    @objc private func editingDidEnd() {
        updateAppearance(animated: usesAnimation)
    }

    private func updateAppearance(animated: Bool) {
        let isActive = isFirstResponder

        let borderColor = (isActive ? activeBorderColor : inactiveBorderColor).cgColor
        let borderWidth = isActive ? activeBorderWidth : inactiveBorderWidth
        let bgColor = isActive ? activeBackgroundColor : inactiveBackgroundColor

        let apply = { [weak self] in
            guard let self else { return }
            layer.cornerRadius = cornerRadius
            layer.masksToBounds = true

            if showsBorder {
                layer.borderColor = borderColor
                layer.borderWidth = borderWidth
            } else {
                layer.borderColor = UIColor.clear.cgColor
                layer.borderWidth = 0
            }

            backgroundColor = bgColor
        }

        guard animated else {
            apply()
            return
        }

        UIView.animate(withDuration: animationDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseInOut]) {
            apply()
            self.layoutIfNeeded()
        }
    }
}

これでstoryboardから直接プロパティを指定しつつフォーカス時の表示制御ができるようになりました。早速使ってみましょう。

実行結果

普段は黄色背景で、フォーカスすると白背景と太線青枠になり、フォーカスを外すとまた元に戻りました。良かったですね。