ValidatorKit
SwiftUI・UIKit両対応の軽量バリデーションライブラリ。流れるようなAPIでルール定義が可能。
GitHub概要
Alhiane/ValidatorKit
ValidatorKit is a straightforward validation library for Swift, designed to simplify data validation rules in iOS applications. It provides an easy way to ensure user input meets your specified criteria.
スター7
ウォッチ2
フォーク0
作成日:2024年9月22日
言語:Swift
ライセンス:MIT License
トピックス
ios-swiftswiftswiftuivalidation-libraryvalidation-rules
スター履歴
データ取得日時: 2025/10/22 09:50
ValidatorKit
ValidatorKitは、SwiftUIとUIKitの両方に対応した軽量で強力なバリデーションライブラリです。流れるようなAPIデザインにより、直感的で読みやすいバリデーションルールの定義が可能です。
特徴
- SwiftUI・UIKit両対応: どちらのUIフレームワークでも使用可能
- 軽量設計: 最小限の依存関係で高いパフォーマンス
- 流れるようなAPI: 直感的で読みやすいメソッドチェーン
- 組み込みバリデーター: 一般的なバリデーションルールを内蔵
- カスタムバリデーター: 独自のバリデーションロジックを簡単に追加
- エラーハンドリング: 詳細なエラー情報とローカライゼーション対応
- 非同期対応: 重いバリデーション処理の非同期実行
- テスト可能: ユニットテストの作成が容易
インストール
Swift Package Manager
// Package.swift
dependencies: [
.package(url: "https://github.com/alhiane/ValidatorKit.git", from: "1.0.0")
]
Xcodeでのインストール:
- File → Swift Packages → Add Package Dependency
https://github.com/alhiane/ValidatorKit.gitを入力- バージョンを選択してプロジェクトに追加
CocoaPods
# Podfile
pod 'ValidatorKit', '~> 1.0'
pod install
Carthage
# Cartfile
github "alhiane/ValidatorKit" ~> 1.0
carthage update
基本的な使用方法
1. 基本的なバリデーション
import ValidatorKit
// 基本的なバリデーション
let emailValidator = Validator()
.email()
.required()
let result = emailValidator.validate("[email protected]")
switch result {
case .success:
print("バリデーション成功")
case .failure(let errors):
print("エラー: \(errors.map { $0.localizedDescription })")
}
2. 複数ルールのチェーン
// 複数のバリデーションルールをチェーン
let passwordValidator = Validator()
.required(message: "パスワードは必須です")
.minLength(8, message: "8文字以上で入力してください")
.contains(.uppercaseLetters, message: "大文字を含んでください")
.contains(.lowercaseLetters, message: "小文字を含んでください")
.contains(.digits, message: "数字を含んでください")
.contains(.specialCharacters, message: "記号を含んでください")
let password = "MyPassword123!"
let result = passwordValidator.validate(password)
3. カスタムバリデーション
// カスタムバリデーションルール
let customValidator = Validator()
.custom { value in
guard value.count >= 3 else {
return .failure("3文字以上で入力してください")
}
guard value.allSatisfy({ $0.isLetter }) else {
return .failure("文字のみ使用できます")
}
return .success
}
let result = customValidator.validate("abc")
組み込みバリデーター
基本バリデーター
// 必須フィールド
Validator().required()
Validator().required(message: "この項目は必須です")
// 空文字チェック
Validator().notEmpty()
Validator().notEmpty(message: "空文字は許可されていません")
// 文字数制限
Validator().minLength(5)
Validator().maxLength(50)
Validator().length(exactly: 10)
Validator().length(min: 5, max: 50)
フォーマットバリデーター
// メールアドレス
Validator().email()
Validator().email(message: "正しいメールアドレスを入力してください")
// URL
Validator().url()
Validator().url(schemes: ["http", "https"])
// 数値
Validator().numeric()
Validator().integer()
Validator().decimal()
パターンバリデーター
// 正規表現
Validator().matches(pattern: "^[0-9]+$")
Validator().matches(regex: try! NSRegularExpression(pattern: "^\\d{3}-\\d{4}$"))
// 文字種別
Validator().contains(.uppercaseLetters)
Validator().contains(.lowercaseLetters)
Validator().contains(.digits)
Validator().contains(.specialCharacters)
Validator().contains(.whitespaces)
// 英数字のみ
Validator().alphanumeric()
Validator().alphabetic()
比較バリデーター
// 範囲チェック
Validator().range(1...100)
Validator().range(min: 18, max: 120)
// 最小・最大値
Validator().min(0)
Validator().max(999)
// 確認フィールド
Validator().equals(to: passwordField.text)
Validator().equals(to: "期待値")
カスタムバリデーター
1. 基本的なカスタムバリデーター
extension Validator {
func japanesePhoneNumber() -> Self {
return custom { value in
let pattern = "^(070|080|090)-\\d{4}-\\d{4}$"
let regex = try! NSRegularExpression(pattern: pattern)
let range = NSRange(location: 0, length: value.utf16.count)
if regex.firstMatch(in: value, options: [], range: range) != nil {
return .success
} else {
return .failure("正しい日本の携帯電話番号を入力してください(例: 090-1234-5678)")
}
}
}
}
// 使用例
let phoneValidator = Validator()
.required()
.japanesePhoneNumber()
2. 再利用可能なカスタムバリデーター
struct AgeValidator: ValidatorProtocol {
let minAge: Int
let maxAge: Int
init(min: Int = 0, max: Int = 120) {
self.minAge = min
self.maxAge = max
}
func validate(_ value: String) -> ValidationResult {
guard let age = Int(value) else {
return .failure("数値を入力してください")
}
guard age >= minAge else {
return .failure("\(minAge)歳以上で入力してください")
}
guard age <= maxAge else {
return .failure("\(maxAge)歳以下で入力してください")
}
return .success
}
}
// 使用例
let ageValidator = Validator()
.required()
.add(AgeValidator(min: 18, max: 65))
3. 非同期バリデーター
extension Validator {
func uniqueUsername() -> Self {
return asyncCustom { value in
return await withCheckedContinuation { continuation in
// API呼び出しでユーザー名の重複チェック
UserService.checkUsernameAvailability(value) { isAvailable in
if isAvailable {
continuation.resume(returning: .success)
} else {
continuation.resume(returning: .failure("このユーザー名は既に使用されています"))
}
}
}
}
}
}
バリデーションルールのチェーン
1. 条件付きバリデーション
let conditionalValidator = Validator()
.required()
.if({ $0.count > 0 }) {
$0.email()
}
.else {
$0.custom { _ in .failure("メールアドレスを入力してください") }
}
2. OR条件バリデーション
let orValidator = Validator()
.either {
$0.email()
} or: {
$0.matches(pattern: "^\\d{3}-\\d{4}-\\d{4}$") // 電話番号形式
}
3. 複合バリデーション
let complexValidator = Validator()
.required()
.minLength(8)
.all([
Validator().contains(.uppercaseLetters),
Validator().contains(.lowercaseLetters),
Validator().contains(.digits)
])
.none([
Validator().contains("password"),
Validator().contains("123456")
])
エラーハンドリング
1. エラー情報の取得
let validator = Validator()
.required(message: "必須項目です")
.email(message: "正しいメールアドレスを入力してください")
let result = validator.validate("invalid-email")
switch result {
case .success:
print("バリデーション成功")
case .failure(let errors):
for error in errors {
print("エラー: \(error.message)")
print("ルール: \(error.rule)")
print("入力値: \(error.value)")
}
}
2. エラーメッセージのカスタマイズ
// ローカライゼーション対応
extension ValidationError {
var localizedMessage: String {
return NSLocalizedString(self.message, comment: "")
}
}
// カスタムエラータイプ
enum CustomValidationError: ValidationErrorProtocol {
case tooShort(minimum: Int)
case invalidFormat(expected: String)
case networkError(underlying: Error)
var message: String {
switch self {
case .tooShort(let min):
return "最低\(min)文字で入力してください"
case .invalidFormat(let expected):
return "形式: \(expected)"
case .networkError(let error):
return "ネットワークエラー: \(error.localizedDescription)"
}
}
}
3. エラーの集約と表示
struct ValidationErrorHandler {
static func handle(_ errors: [ValidationError]) -> String {
let messages = errors.map { $0.localizedMessage }
return messages.joined(separator: "\n")
}
static func showAlert(for errors: [ValidationError], on viewController: UIViewController) {
let message = handle(errors)
let alert = UIAlertController(
title: "入力エラー",
message: message,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
viewController.present(alert, animated: true)
}
}
SwiftUIとの統合
1. 基本的なフォームバリデーション
import SwiftUI
import ValidatorKit
struct RegistrationForm: View {
@State private var email = ""
@State private var password = ""
@State private var confirmPassword = ""
@State private var emailErrors: [ValidationError] = []
@State private var passwordErrors: [ValidationError] = []
@State private var confirmPasswordErrors: [ValidationError] = []
private let emailValidator = Validator()
.required(message: "メールアドレスは必須です")
.email(message: "正しいメールアドレスを入力してください")
private let passwordValidator = Validator()
.required(message: "パスワードは必須です")
.minLength(8, message: "8文字以上で入力してください")
.contains(.uppercaseLetters, message: "大文字を含んでください")
.contains(.digits, message: "数字を含んでください")
var body: some View {
NavigationView {
Form {
Section(header: Text("アカウント情報")) {
VStack(alignment: .leading) {
TextField("メールアドレス", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: email) { _ in
validateEmail()
}
ForEach(emailErrors, id: \.message) { error in
Text(error.message)
.foregroundColor(.red)
.font(.caption)
}
}
VStack(alignment: .leading) {
SecureField("パスワード", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: password) { _ in
validatePassword()
validateConfirmPassword()
}
ForEach(passwordErrors, id: \.message) { error in
Text(error.message)
.foregroundColor(.red)
.font(.caption)
}
}
VStack(alignment: .leading) {
SecureField("パスワード確認", text: $confirmPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: confirmPassword) { _ in
validateConfirmPassword()
}
ForEach(confirmPasswordErrors, id: \.message) { error in
Text(error.message)
.foregroundColor(.red)
.font(.caption)
}
}
}
Section {
Button("登録") {
submitForm()
}
.disabled(!isFormValid)
}
}
.navigationTitle("ユーザー登録")
}
}
private var isFormValid: Bool {
return emailErrors.isEmpty &&
passwordErrors.isEmpty &&
confirmPasswordErrors.isEmpty &&
!email.isEmpty &&
!password.isEmpty &&
!confirmPassword.isEmpty
}
private func validateEmail() {
let result = emailValidator.validate(email)
switch result {
case .success:
emailErrors = []
case .failure(let errors):
emailErrors = errors
}
}
private func validatePassword() {
let result = passwordValidator.validate(password)
switch result {
case .success:
passwordErrors = []
case .failure(let errors):
passwordErrors = errors
}
}
private func validateConfirmPassword() {
let confirmValidator = Validator()
.required(message: "パスワード確認は必須です")
.equals(to: password, message: "パスワードが一致しません")
let result = confirmValidator.validate(confirmPassword)
switch result {
case .success:
confirmPasswordErrors = []
case .failure(let errors):
confirmPasswordErrors = errors
}
}
private func submitForm() {
// フォーム送信処理
validateEmail()
validatePassword()
validateConfirmPassword()
if isFormValid {
// API呼び出しなど
print("フォーム送信: \(email)")
}
}
}
2. カスタムValidationTextFieldコンポーネント
struct ValidationTextField: View {
let title: String
@Binding var text: String
let validator: Validator
@State private var errors: [ValidationError] = []
var body: some View {
VStack(alignment: .leading, spacing: 4) {
TextField(title, text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(errors.isEmpty ? Color.gray : Color.red, width: 1)
.onChange(of: text) { _ in
validate()
}
if !errors.isEmpty {
VStack(alignment: .leading, spacing: 2) {
ForEach(errors, id: \.message) { error in
HStack {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(.red)
.font(.caption)
Text(error.message)
.foregroundColor(.red)
.font(.caption)
}
}
}
}
}
}
private func validate() {
let result = validator.validate(text)
switch result {
case .success:
errors = []
case .failure(let validationErrors):
errors = validationErrors
}
}
}
// 使用例
ValidationTextField(
title: "メールアドレス",
text: $email,
validator: Validator()
.required()
.email()
)
UIKitとの統合
1. UITextFieldでのリアルタイムバリデーション
import UIKit
import ValidatorKit
class FormViewController: UIViewController {
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var emailErrorLabel: UILabel!
@IBOutlet weak var passwordErrorLabel: UILabel!
@IBOutlet weak var submitButton: UIButton!
private let emailValidator = Validator()
.required(message: "メールアドレスは必須です")
.email(message: "正しいメールアドレスを入力してください")
private let passwordValidator = Validator()
.required(message: "パスワードは必須です")
.minLength(8, message: "8文字以上で入力してください")
.contains(.uppercaseLetters, message: "大文字を含んでください")
.contains(.digits, message: "数字を含んでください")
override func viewDidLoad() {
super.viewDidLoad()
setupTextFields()
updateSubmitButton()
}
private func setupTextFields() {
emailTextField.addTarget(self, action: #selector(emailTextChanged), for: .editingChanged)
passwordTextField.addTarget(self, action: #selector(passwordTextChanged), for: .editingChanged)
// 初期状態でエラーラベルを非表示
emailErrorLabel.isHidden = true
passwordErrorLabel.isHidden = true
}
@objc private func emailTextChanged() {
validateEmail()
updateSubmitButton()
}
@objc private func passwordTextChanged() {
validatePassword()
updateSubmitButton()
}
private func validateEmail() {
let result = emailValidator.validate(emailTextField.text ?? "")
switch result {
case .success:
emailErrorLabel.isHidden = true
emailTextField.layer.borderColor = UIColor.systemGreen.cgColor
emailTextField.layer.borderWidth = 1.0
case .failure(let errors):
emailErrorLabel.text = errors.first?.message
emailErrorLabel.isHidden = false
emailTextField.layer.borderColor = UIColor.systemRed.cgColor
emailTextField.layer.borderWidth = 1.0
}
}
private func validatePassword() {
let result = passwordValidator.validate(passwordTextField.text ?? "")
switch result {
case .success:
passwordErrorLabel.isHidden = true
passwordTextField.layer.borderColor = UIColor.systemGreen.cgColor
passwordTextField.layer.borderWidth = 1.0
case .failure(let errors):
passwordErrorLabel.text = errors.first?.message
passwordErrorLabel.isHidden = false
passwordTextField.layer.borderColor = UIColor.systemRed.cgColor
passwordTextField.layer.borderWidth = 1.0
}
}
private func updateSubmitButton() {
let emailValid = emailValidator.validate(emailTextField.text ?? "").isSuccess
let passwordValid = passwordValidator.validate(passwordTextField.text ?? "").isSuccess
submitButton.isEnabled = emailValid && passwordValid
submitButton.alpha = submitButton.isEnabled ? 1.0 : 0.5
}
@IBAction func submitButtonTapped(_ sender: UIButton) {
// 最終バリデーション
validateEmail()
validatePassword()
if submitButton.isEnabled {
// フォーム送信処理
submitForm()
}
}
private func submitForm() {
// API呼び出しなど
print("フォーム送信: \(emailTextField.text ?? "")")
}
}
// ValidationResult拡張
extension ValidationResult {
var isSuccess: Bool {
switch self {
case .success:
return true
case .failure:
return false
}
}
}
2. カスタムValidationTextFieldクラス
class ValidationTextField: UITextField {
private var validator: Validator?
private var errorLabel: UILabel?
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
addTarget(self, action: #selector(textChanged), for: .editingChanged)
layer.borderWidth = 1.0
layer.cornerRadius = 8.0
layer.borderColor = UIColor.systemGray4.cgColor
}
func setValidator(_ validator: Validator) {
self.validator = validator
}
func setErrorLabel(_ label: UILabel) {
self.errorLabel = label
label.isHidden = true
label.textColor = .systemRed
label.font = UIFont.systemFont(ofSize: 12)
}
@objc private func textChanged() {
validate()
}
@discardableResult
func validate() -> Bool {
guard let validator = validator else { return true }
let result = validator.validate(text ?? "")
switch result {
case .success:
layer.borderColor = UIColor.systemGreen.cgColor
errorLabel?.isHidden = true
return true
case .failure(let errors):
layer.borderColor = UIColor.systemRed.cgColor
errorLabel?.text = errors.first?.message
errorLabel?.isHidden = false
return false
}
}
}
実践的な例
1. ユーザー登録フォーム
import ValidatorKit
class RegistrationManager {
// バリデーターの定義
private let usernameValidator = Validator()
.required(message: "ユーザー名は必須です")
.minLength(3, message: "ユーザー名は3文字以上で入力してください")
.maxLength(20, message: "ユーザー名は20文字以下で入力してください")
.alphanumeric(message: "ユーザー名は英数字のみ使用できます")
private let emailValidator = Validator()
.required(message: "メールアドレスは必須です")
.email(message: "正しいメールアドレスを入力してください")
private let passwordValidator = Validator()
.required(message: "パスワードは必須です")
.minLength(8, message: "パスワードは8文字以上で入力してください")
.contains(.uppercaseLetters, message: "大文字を含んでください")
.contains(.lowercaseLetters, message: "小文字を含んでください")
.contains(.digits, message: "数字を含んでください")
.contains(.specialCharacters, message: "記号を含んでください")
private let phoneValidator = Validator()
.required(message: "電話番号は必須です")
.japanesePhoneNumber()
private let ageValidator = Validator()
.required(message: "年齢は必須です")
.add(AgeValidator(min: 18, max: 120))
func validateRegistrationForm(
username: String,
email: String,
password: String,
confirmPassword: String,
phone: String,
age: String
) -> RegistrationValidationResult {
var errors: [String: [ValidationError]] = [:]
// 各フィールドのバリデーション
if case .failure(let usernameErrors) = usernameValidator.validate(username) {
errors["username"] = usernameErrors
}
if case .failure(let emailErrors) = emailValidator.validate(email) {
errors["email"] = emailErrors
}
if case .failure(let passwordErrors) = passwordValidator.validate(password) {
errors["password"] = passwordErrors
}
// パスワード確認
let confirmPasswordValidator = Validator()
.required(message: "パスワード確認は必須です")
.equals(to: password, message: "パスワードが一致しません")
if case .failure(let confirmErrors) = confirmPasswordValidator.validate(confirmPassword) {
errors["confirmPassword"] = confirmErrors
}
if case .failure(let phoneErrors) = phoneValidator.validate(phone) {
errors["phone"] = phoneErrors
}
if case .failure(let ageErrors) = ageValidator.validate(age) {
errors["age"] = ageErrors
}
return RegistrationValidationResult(errors: errors)
}
}
struct RegistrationValidationResult {
let errors: [String: [ValidationError]]
var isValid: Bool {
return errors.isEmpty
}
func errorsFor(field: String) -> [ValidationError] {
return errors[field] ?? []
}
func firstErrorFor(field: String) -> String? {
return errors[field]?.first?.message
}
}
2. クレジットカード情報バリデーション
extension Validator {
func creditCardNumber() -> Self {
return custom { value in
let cleanedNumber = value.replacingOccurrences(of: " ", with: "")
// Luhnアルゴリズムでバリデーション
if isValidCreditCardNumber(cleanedNumber) {
return .success
} else {
return .failure("正しいクレジットカード番号を入力してください")
}
}
}
func creditCardExpiry() -> Self {
return custom { value in
let pattern = "^(0[1-9]|1[0-2])\\/([0-9]{2})$"
let regex = try! NSRegularExpression(pattern: pattern)
let range = NSRange(location: 0, length: value.utf16.count)
guard regex.firstMatch(in: value, options: [], range: range) != nil else {
return .failure("MM/YY の形式で入力してください")
}
// 有効期限のチェック
let components = value.split(separator: "/")
let month = Int(components[0]) ?? 0
let year = 2000 + (Int(components[1]) ?? 0)
let currentDate = Date()
let calendar = Calendar.current
let currentYear = calendar.component(.year, from: currentDate)
let currentMonth = calendar.component(.month, from: currentDate)
if year < currentYear || (year == currentYear && month < currentMonth) {
return .failure("有効期限が過ぎています")
}
return .success
}
}
func cvv() -> Self {
return custom { value in
let pattern = "^[0-9]{3,4}$"
let regex = try! NSRegularExpression(pattern: pattern)
let range = NSRange(location: 0, length: value.utf16.count)
if regex.firstMatch(in: value, options: [], range: range) != nil {
return .success
} else {
return .failure("3-4桁の数字を入力してください")
}
}
}
}
private func isValidCreditCardNumber(_ number: String) -> Bool {
// Luhnアルゴリズムの実装
let digits = number.compactMap { Int(String($0)) }
guard digits.count >= 13 && digits.count <= 19 else { return false }
var sum = 0
let reversedDigits = digits.reversed()
for (index, digit) in reversedDigits.enumerated() {
if index % 2 == 1 {
let doubled = digit * 2
sum += doubled > 9 ? doubled - 9 : doubled
} else {
sum += digit
}
}
return sum % 10 == 0
}
テスト
1. ユニットテスト
import XCTest
@testable import ValidatorKit
class ValidatorKitTests: XCTestCase {
func testEmailValidation() {
let validator = Validator().email()
// 有効なメールアドレス
XCTAssertEqual(validator.validate("[email protected]"), .success)
XCTAssertEqual(validator.validate("[email protected]"), .success)
// 無効なメールアドレス
XCTAssertNotEqual(validator.validate("invalid-email"), .success)
XCTAssertNotEqual(validator.validate("test@"), .success)
XCTAssertNotEqual(validator.validate("@example.com"), .success)
}
func testPasswordStrengthValidation() {
let validator = Validator()
.minLength(8)
.contains(.uppercaseLetters)
.contains(.lowercaseLetters)
.contains(.digits)
.contains(.specialCharacters)
// 強力なパスワード
XCTAssertEqual(validator.validate("MyPassw0rd!"), .success)
XCTAssertEqual(validator.validate("Secure123@"), .success)
// 弱いパスワード
XCTAssertNotEqual(validator.validate("password"), .success) // 大文字・数字・記号なし
XCTAssertNotEqual(validator.validate("PASSWORD123"), .success) // 小文字・記号なし
XCTAssertNotEqual(validator.validate("Pass1!"), .success) // 短すぎる
}
func testCustomValidation() {
let ageValidator = Validator().add(AgeValidator(min: 18, max: 65))
// 有効な年齢
XCTAssertEqual(ageValidator.validate("25"), .success)
XCTAssertEqual(ageValidator.validate("18"), .success)
XCTAssertEqual(ageValidator.validate("65"), .success)
// 無効な年齢
XCTAssertNotEqual(ageValidator.validate("17"), .success)
XCTAssertNotEqual(ageValidator.validate("66"), .success)
XCTAssertNotEqual(ageValidator.validate("abc"), .success)
}
func testChainedValidation() {
let validator = Validator()
.required()
.minLength(5)
.maxLength(20)
.alphanumeric()
// 有効な入力
XCTAssertEqual(validator.validate("user123"), .success)
XCTAssertEqual(validator.validate("testUser"), .success)
// 無効な入力
XCTAssertNotEqual(validator.validate(""), .success) // 必須
XCTAssertNotEqual(validator.validate("test"), .success) // 短すぎる
XCTAssertNotEqual(validator.validate("verylongusernamethatexceedslimit"), .success) // 長すぎる
XCTAssertNotEqual(validator.validate("user-name"), .success) // 英数字以外
}
func testErrorMessages() {
let validator = Validator()
.required(message: "カスタム必須メッセージ")
.email(message: "カスタムメールメッセージ")
let result = validator.validate("invalid")
if case .failure(let errors) = result {
XCTAssertTrue(errors.contains { $0.message == "カスタムメールメッセージ" })
} else {
XCTFail("バリデーションが成功してしまいました")
}
}
}
2. SwiftUIテスト
import XCTest
import SwiftUI
@testable import ValidatorKit
class SwiftUIValidationTests: XCTestCase {
func testValidationTextFieldState() {
let validator = Validator().required().email()
let textBinding = Binding<String>(
get: { "[email protected]" },
set: { _ in }
)
let textField = ValidationTextField(
title: "Email",
text: textBinding,
validator: validator
)
// ビューの状態をテスト
// 実際のSwiftUIテストでは ViewInspector などのライブラリを使用
}
}
ベストプラクティス
1. パフォーマンスの最適化
// 重いバリデーションは遅延実行
let debouncedValidator = Validator()
.required()
.email()
.debounce(0.5) // 0.5秒後に実行
// キャッシュ機能付きバリデーション
class CachedValidator {
private var cache: [String: ValidationResult] = [:]
private let validator: Validator
init(validator: Validator) {
self.validator = validator
}
func validate(_ value: String) -> ValidationResult {
if let cachedResult = cache[value] {
return cachedResult
}
let result = validator.validate(value)
cache[value] = result
return result
}
}
2. エラーメッセージの国際化
// Localizable.strings
"validation.required" = "この項目は必須です";
"validation.email" = "正しいメールアドレスを入力してください";
"validation.minLength" = "最低%d文字で入力してください";
// LocalizedValidator
struct LocalizedValidator {
static func required() -> Validator {
return Validator().required(
message: NSLocalizedString("validation.required", comment: "")
)
}
static func email() -> Validator {
return Validator().email(
message: NSLocalizedString("validation.email", comment: "")
)
}
static func minLength(_ length: Int) -> Validator {
let message = String(format: NSLocalizedString("validation.minLength", comment: ""), length)
return Validator().minLength(length, message: message)
}
}
3. アクセシビリティ対応
// SwiftUIでのアクセシビリティ
ValidationTextField(title: "メールアドレス", text: $email, validator: emailValidator)
.accessibilityLabel("メールアドレス入力フィールド")
.accessibilityHint("正しいメールアドレスを入力してください")
// UIKitでのアクセシビリティ
validationTextField.accessibilityLabel = "メールアドレス"
validationTextField.accessibilityHint = "正しいメールアドレスを入力してください"
errorLabel.accessibilityTraits = .staticText
まとめ
ValidatorKitは、SwiftUIとUIKitの両方で使用できる強力で軽量なバリデーションライブラリです。流れるようなAPIデザインにより、複雑なバリデーションルールも直感的に定義できます。
主な利点
- 直感的なAPI: 読みやすく書きやすいメソッドチェーン
- 軽量設計: 最小限の依存関係で高いパフォーマンス
- 柔軟性: 豊富な組み込みバリデーターとカスタマイズ性
- UI統合: SwiftUIとUIKitの両方に対応
- エラーハンドリング: 詳細なエラー情報と国際化対応
- テスタビリティ: ユニットテストの作成が容易
ValidatorKitを使用することで、堅牢で使いやすいフォームバリデーション機能をSwiftアプリケーションに効率的に実装できます。