Validator

import { Badge } from '../../../../../components/ui/badge';

Validator

Validation Swift iOS

概要

Validatorは、Swift向けのシンプルで柔軟なバリデーションライブラリです。iOS、macOS、tvOS、watchOSアプリケーションでユーザー入力の検証を簡単に実装できます。宣言的なAPIと豊富な組み込みバリデーションルールを提供し、カスタムバリデーションの作成も容易です。

主な特徴

  • 宣言的なAPI - 直感的で読みやすいバリデーションルールの定義
  • 豊富な組み込みルール - よく使用されるバリデーションパターンを事前定義
  • カスタムバリデーション - 独自のバリデーションロジックを簡単に追加
  • エラーメッセージ - カスタマイズ可能なエラーメッセージ
  • チェーン可能 - 複数のバリデーションルールを組み合わせ可能
  • 型安全 - Swiftの型システムを活用した安全なバリデーション

インストール

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/adamwaite/Validator.git", from: "3.2.1")
]

CocoaPods

pod 'Validator'

Carthage

github "adamwaite/Validator"

基本的な使い方

シンプルなバリデーション

import Validator

// メールアドレスのバリデーション
let emailRule = ValidationRulePattern(
    pattern: EmailValidationPattern(),
    error: ValidationError(message: "無効なメールアドレスです")
)

let result = "[email protected]".validate(rule: emailRule)

switch result {
case .valid:
    print("有効なメールアドレスです")
case .invalid(let errors):
    errors.forEach { print($0.message) }
}

テキストフィールドのバリデーション

class LoginViewController: UIViewController {
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    
    func setupValidation() {
        // メールアドレスのバリデーションルール
        emailTextField.validationRules = ValidationRuleSet<String>()
        emailTextField.validationRules?.add(rule: ValidationRuleRequired<String>(
            error: ValidationError(message: "メールアドレスは必須です")
        ))
        emailTextField.validationRules?.add(rule: ValidationRulePattern(
            pattern: EmailValidationPattern(),
            error: ValidationError(message: "有効なメールアドレスを入力してください")
        ))
        
        // パスワードのバリデーションルール
        passwordTextField.validationRules = ValidationRuleSet<String>()
        passwordTextField.validationRules?.add(rule: ValidationRuleLength(
            min: 8,
            max: 20,
            error: ValidationError(message: "パスワードは8〜20文字で入力してください")
        ))
    }
    
    func validateForm() -> Bool {
        let emailResult = emailTextField.validate()
        let passwordResult = passwordTextField.validate()
        
        return emailResult.isValid && passwordResult.isValid
    }
}

組み込みバリデーションルール

必須チェック

let requiredRule = ValidationRuleRequired<String>(
    error: ValidationError(message: "このフィールドは必須です")
)

文字数制限

// 最小文字数
let minLengthRule = ValidationRuleLength(
    min: 3,
    error: ValidationError(message: "3文字以上入力してください")
)

// 最大文字数
let maxLengthRule = ValidationRuleLength(
    max: 100,
    error: ValidationError(message: "100文字以内で入力してください")
)

// 範囲指定
let rangeLengthRule = ValidationRuleLength(
    min: 3,
    max: 20,
    error: ValidationError(message: "3〜20文字で入力してください")
)

パターンマッチング

// メールアドレス
let emailRule = ValidationRulePattern(
    pattern: EmailValidationPattern(),
    error: ValidationError(message: "無効なメールアドレスです")
)

// アルファベットのみ
let alphaRule = ValidationRulePattern(
    pattern: AlphaValidationPattern(),
    error: ValidationError(message: "アルファベットのみ入力してください")
)

// 数字のみ
let numericRule = ValidationRulePattern(
    pattern: NumericValidationPattern(),
    error: ValidationError(message: "数字のみ入力してください")
)

// アルファベットと数字
let alphaNumericRule = ValidationRulePattern(
    pattern: AlphaNumericValidationPattern(),
    error: ValidationError(message: "英数字のみ入力してください")
)

数値の範囲

let ageRule = ValidationRuleComparison<Int>(
    min: 18,
    max: 100,
    error: ValidationError(message: "年齢は18〜100歳の範囲で入力してください")
)

カスタムバリデーション

カスタムパターン

// 日本の郵便番号パターン
struct JapanesePostalCodePattern: ValidationPattern {
    func evaluate(_ input: String) -> Bool {
        let regex = "^\\d{3}-\\d{4}$"
        return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: input)
    }
}

let postalCodeRule = ValidationRulePattern(
    pattern: JapanesePostalCodePattern(),
    error: ValidationError(message: "郵便番号は123-4567の形式で入力してください")
)

カスタムルール

// パスワード強度チェック
struct PasswordStrengthRule: ValidationRule {
    typealias InputType = String
    
    var error: ValidationError
    
    func evaluate(with input: String?) -> Bool {
        guard let password = input else { return false }
        
        // 大文字、小文字、数字、特殊文字を含むかチェック
        let hasUpperCase = password.contains { $0.isUppercase }
        let hasLowerCase = password.contains { $0.isLowercase }
        let hasNumber = password.contains { $0.isNumber }
        let hasSpecialChar = password.contains { !$0.isLetterOrNumber }
        
        return hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar
    }
}

let passwordStrengthRule = PasswordStrengthRule(
    error: ValidationError(message: "パスワードは大文字、小文字、数字、特殊文字を含む必要があります")
)

条件付きバリデーション

// 他のフィールドの値に基づくバリデーション
class ConditionalValidationRule: ValidationRule {
    typealias InputType = String
    
    var error: ValidationError
    let condition: () -> Bool
    let rule: ValidationRule
    
    init(condition: @escaping () -> Bool, rule: ValidationRule, error: ValidationError) {
        self.condition = condition
        self.rule = rule
        self.error = error
    }
    
    func evaluate(with input: String?) -> Bool {
        if condition() {
            return rule.evaluate(with: input)
        }
        return true
    }
}

複数ルールの組み合わせ

ValidationRuleSet

// ユーザー名のバリデーションセット
let usernameRules = ValidationRuleSet<String>()

// 必須チェック
usernameRules.add(rule: ValidationRuleRequired<String>(
    error: ValidationError(message: "ユーザー名は必須です")
))

// 文字数制限
usernameRules.add(rule: ValidationRuleLength(
    min: 3,
    max: 20,
    error: ValidationError(message: "ユーザー名は3〜20文字で入力してください")
))

// 使用可能文字
usernameRules.add(rule: ValidationRulePattern(
    pattern: AlphaNumericValidationPattern(),
    error: ValidationError(message: "ユーザー名は英数字のみ使用できます")
))

// バリデーション実行
let result = "username123".validate(rules: usernameRules)

エラーハンドリング

カスタムエラーメッセージ

struct LocalizedValidationError: ValidationError {
    let key: String
    
    var message: String {
        return NSLocalizedString(key, comment: "")
    }
}

let emailRule = ValidationRulePattern(
    pattern: EmailValidationPattern(),
    error: LocalizedValidationError(key: "error.invalid_email")
)

エラー表示

extension UITextField {
    func showValidationErrors(_ errors: [ValidationError]) {
        // エラーメッセージを表示
        let errorMessage = errors.map { $0.message }.joined(separator: "\n")
        
        // エラーラベルに表示
        errorLabel.text = errorMessage
        errorLabel.isHidden = false
        
        // ボーダーを赤に変更
        layer.borderColor = UIColor.red.cgColor
        layer.borderWidth = 1.0
    }
    
    func clearValidationErrors() {
        errorLabel.isHidden = true
        layer.borderColor = UIColor.clear.cgColor
        layer.borderWidth = 0.0
    }
}

フォームバリデーション

フォームバリデーター

class FormValidator {
    private var validationRules: [UITextField: ValidationRuleSet<String>] = [:]
    
    func addRule(for textField: UITextField, rules: ValidationRuleSet<String>) {
        validationRules[textField] = rules
    }
    
    func validate() -> (isValid: Bool, errors: [UITextField: [ValidationError]]) {
        var allErrors: [UITextField: [ValidationError]] = [:]
        var isValid = true
        
        for (textField, rules) in validationRules {
            let result = textField.text?.validate(rules: rules) ?? .invalid([])
            
            switch result {
            case .valid:
                textField.clearValidationErrors()
            case .invalid(let errors):
                isValid = false
                allErrors[textField] = errors
                textField.showValidationErrors(errors)
            }
        }
        
        return (isValid, allErrors)
    }
}

リアルタイムバリデーション

class SignUpViewController: UIViewController {
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var confirmPasswordTextField: UITextField!
    
    private let formValidator = FormValidator()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupValidation()
        setupTextFieldObservers()
    }
    
    private func setupTextFieldObservers() {
        // リアルタイムバリデーション
        emailTextField.addTarget(
            self,
            action: #selector(textFieldDidChange(_:)),
            for: .editingChanged
        )
        
        passwordTextField.addTarget(
            self,
            action: #selector(textFieldDidChange(_:)),
            for: .editingChanged
        )
    }
    
    @objc private func textFieldDidChange(_ textField: UITextField) {
        // 入力中のバリデーション
        if let rules = textField.validationRules {
            let result = textField.text?.validate(rules: rules) ?? .invalid([])
            
            switch result {
            case .valid:
                textField.clearValidationErrors()
            case .invalid(let errors):
                // 入力中は軽いフィードバックのみ
                textField.layer.borderColor = UIColor.orange.cgColor
            }
        }
    }
}

高度な使用例

非同期バリデーション

class AsyncValidationRule: ValidationRule {
    typealias InputType = String
    
    var error: ValidationError
    private let validationClosure: (String, @escaping (Bool) -> Void) -> Void
    
    init(
        error: ValidationError,
        validation: @escaping (String, @escaping (Bool) -> Void) -> Void
    ) {
        self.error = error
        self.validationClosure = validation
    }
    
    func validateAsync(input: String?, completion: @escaping (ValidationResult) -> Void) {
        guard let input = input else {
            completion(.invalid([error]))
            return
        }
        
        validationClosure(input) { isValid in
            if isValid {
                completion(.valid)
            } else {
                completion(.invalid([self.error]))
            }
        }
    }
}

// 使用例:ユーザー名の重複チェック
let usernameAvailableRule = AsyncValidationRule(
    error: ValidationError(message: "このユーザー名は既に使用されています")
) { username, completion in
    // APIコール
    APIClient.checkUsernameAvailability(username) { isAvailable in
        completion(isAvailable)
    }
}

バリデーション結果の集約

struct ValidationSummary {
    let results: [String: ValidationResult]
    
    var isValid: Bool {
        return results.values.allSatisfy { $0.isValid }
    }
    
    var errors: [String: [ValidationError]] {
        var errorMap: [String: [ValidationError]] = [:]
        
        for (key, result) in results {
            switch result {
            case .invalid(let errors):
                errorMap[key] = errors
            default:
                break
            }
        }
        
        return errorMap
    }
    
    func errorMessage(for key: String) -> String? {
        guard let errors = errors[key] else { return nil }
        return errors.map { $0.message }.joined(separator: ", ")
    }
}

ベストプラクティス

1. バリデーションルールの再利用

// 共通バリデーションルールの定義
struct ValidationRules {
    static let email = ValidationRulePattern(
        pattern: EmailValidationPattern(),
        error: ValidationError(message: "有効なメールアドレスを入力してください")
    )
    
    static let password = ValidationRuleSet<String>()
        .addRule(ValidationRuleRequired<String>(
            error: ValidationError(message: "パスワードは必須です")
        ))
        .addRule(ValidationRuleLength(
            min: 8,
            error: ValidationError(message: "パスワードは8文字以上必要です")
        ))
    
    static let phoneNumber = ValidationRulePattern(
        pattern: PhoneNumberValidationPattern(),
        error: ValidationError(message: "有効な電話番号を入力してください")
    )
}

2. エラーメッセージの国際化

extension ValidationError {
    static func localized(_ key: String) -> ValidationError {
        return ValidationError(message: NSLocalizedString(key, comment: ""))
    }
}

// 使用例
let emailRule = ValidationRulePattern(
    pattern: EmailValidationPattern(),
    error: .localized("validation.error.invalid_email")
)

3. UIとの統合

protocol ValidatableTextField {
    var validationRules: ValidationRuleSet<String>? { get set }
    func validate() -> ValidationResult
    func showError(_ error: String)
    func clearError()
}

extension UITextField: ValidatableTextField {
    private struct AssociatedKeys {
        static var validationRules = "validationRules"
    }
    
    var validationRules: ValidationRuleSet<String>? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.validationRules) as? ValidationRuleSet<String>
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.validationRules, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
    func validate() -> ValidationResult {
        return text?.validate(rules: validationRules ?? ValidationRuleSet()) ?? .invalid([])
    }
}

他のバリデーターとの比較

SwiftValidator vs Validator

機能ValidatorSwiftValidator
宣言的API
組み込みルール豊富標準的
カスタムルール簡単可能
非同期バリデーション拡張可能組み込み
UIとの統合良好良好

選択の指針

  • Validator: シンプルで柔軟なバリデーションが必要な場合
  • SwiftValidator: より包括的な機能が必要な場合
  • 手動実装: 特殊な要件がある場合

まとめ

Validatorは、Swiftアプリケーションにおけるユーザー入力の検証を簡単かつ効果的に実装できる優れたライブラリです。豊富な組み込みルール、柔軟なカスタマイズ性、そして優れたUIとの統合により、高品質なフォームバリデーションを実現できます。