【Swift】UIAlertControllerでバリデーションチェックをする 〜文字数によってエラーメッセージ表示かつボタン非活性〜

18190 ワード

はじめに

UIAlertControllerでバリデーションチェックをする方法です。
バリデーションチェックが必要な入力用のダイアログをUIAlertControllerで作るんだ!っていうのは割とニッチな気はしますがぜひ参考にしてください。

今回実装したのは以下となります。

  • 未入力または入力文字数が5を超えた場合に登録ボタンが非活性になる
  • 入力文字数が5を超えた場合は「5文字以内で入力してください」というエラーメッセージを表示する(エラーメッセージとは別で、通常メッセージは常に表示されている)

環境

Swift5
Xcode13.3
iOS15

実装する

UIAlertControllerを拡張して、必要箇所で呼び出す形で実装します。

拡張クラス

import UIKit

extension UIAlertController {
    static func addAlertWithValidation(
        register: @escaping (_ text: String) -> Void
    ) -> UIAlertController {
        let descriptionString = "入力してください"
        let validationString = "5文字以内で入力してください"
	var alert = UIAlertController()
        var token: NSObjectProtocol?
        
        // UIAlertControllerを作成する
        alert = UIAlertController(title: "登録ダイアログ", message: descriptionString, preferredStyle: .alert)
        
        // 登録時の処理
        let registerAction = UIAlertAction(title: "登録", style: .default, handler: { _ in
            guard let textFields = alert.textFields else { return }
            guard let text = textFields[0].text else { return }
            register(text)
            guard let token = token else { return }
            // オブサーバ登録を解除・・・①
            NotificationCenter.default.removeObserver(token)
        })
        
        // キャンセル時の処理
        let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler: { _ in
            guard let token = token else { return }
            // オブサーバ登録を解除・・・①
            NotificationCenter.default.removeObserver(token)
        })
        
        // テキストフィールドを追加
        alert.addTextField { (textField: UITextField!) -> Void in
            // テキスト変更の通知を受け取るためにオブサーバを登録する・・・②
            token = NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: nil, queue: nil) { _ in
                let text = textField.text ?? ""
                registerAction.isEnabled = false
                if text.count > 5 {
                    // 入力文字が5文字より多い場合(バリデーションエラー)
                    let messageString = "\(descriptionString)\n\(validationString)"
                    let range: NSRange = NSString(string: messageString).range(of: validationString )
                    let alertText = NSMutableAttributedString(string: messageString)
		    // validationStringのみを赤字にする・・・③
                    alertText.addAttributes([
                        .foregroundColor: UIColor.red,
                    ], range: range)
                    alert.setValue(alertText, forKey: "attributedMessage")
                } else {
                    // 入力文字が5文字以内の場合(正常)
                    let alertText = NSMutableAttributedString(string: descriptionString)
                    alert.setValue(alertText, forKey: "attributedMessage")
                    if text.count != 0 {
			// 登録ボタン非活性(未入力時)
                        registerAction.isEnabled = true
                    }
                }
            }
        }
        // 登録ボタン非活性(初期表示)
        registerAction.isEnabled = false
        
        alert.addAction(cancelAction)
        alert.addAction(registerAction)

        return alert
    }
}

ポイントは3点です。

  • 入力テキストフィールドの追加時、入力テキスト変更の通知を受け取るためにオブサーバを登録する(②)
  • バリデーションエラー時、メッセージの文字列の一部を赤色に変更してsetValueすることでエラーメッセージを表現する(③)
  • 登録またはキャンセルボタン押下時のUIAlertActionが実行されるタイミングで、②で登録したオブザーバの解除をする(①)

呼び出し側

こちらは上で作成したメソッドを呼び出すだけとなります。

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func tapButton(_ sender: Any) {
        present(UIAlertController.addAlertWithValidation(
            register: { text in
                // 登録時の処理
            }
        ), animated: true)
    }
}

またメソッドに引数を追加することで、入力文字数の上限を可変にすることもできます。(上限の文字数を渡す、ダイアログのタイプを分類分けしてenum型で渡すetc)
そうすれば使い回しやすくなり、拡張クラスで用意したうまみも大きくなるかと思います。

参考