Foundation バリデーション

Swiftの標準フレームワークであるFoundationが提供する組み込みバリデーション機能

Foundation バリデーションとは

FoundationはSwiftの標準フレームワークで、文字列、数値、日付などの基本的なデータ型に対する包括的なバリデーション機能を提供しています。iOS、macOS、watchOS、tvOSのアプリケーション開発において、追加の依存関係なしにデータ検証を実装できます。

主な特徴

  • 標準フレームワーク: Swiftに標準で含まれており、追加インストール不要
  • 型安全: Swiftの強い型システムを活用した安全なバリデーション
  • プラットフォーム統合: Apple製プラットフォームとの深い統合
  • パフォーマンス: ネイティブ実装による高速な処理
  • 国際化対応: 多言語・多地域に対応したバリデーション

インストールと設定

Foundationは標準フレームワークのため、特別なインストールは不要です:

import Foundation

文字列バリデーション

正規表現を使用したバリデーション

extension String {
    func isValidEmail() -> Bool {
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
        return emailPredicate.evaluate(with: self)
    }
    
    func isValidPhoneNumber() -> Bool {
        let phoneRegex = "^[0-9+\\-\\s()]+$"
        let phonePredicate = NSPredicate(format: "SELF MATCHES %@", phoneRegex)
        return phonePredicate.evaluate(with: self)
    }
}

// 使用例
let email = "[email protected]"
if email.isValidEmail() {
    print("有効なメールアドレスです")
}

文字列の長さと内容のバリデーション

extension String {
    func validateLength(min: Int, max: Int) -> Bool {
        return count >= min && count <= max
    }
    
    func containsOnlyLetters() -> Bool {
        return !isEmpty && rangeOfCharacter(from: CharacterSet.letters.inverted) == nil
    }
    
    func containsOnlyDigits() -> Bool {
        return !isEmpty && rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil
    }
}

数値バリデーション

範囲バリデーション

extension Numeric where Self: Comparable {
    func isInRange(_ range: ClosedRange<Self>) -> Bool {
        return range.contains(self)
    }
}

// 使用例
let age = 25
if age.isInRange(18...65) {
    print("有効な年齢です")
}

数値フォーマットバリデーション

class NumberValidator {
    static func isValidCurrency(_ string: String, locale: Locale = .current) -> Bool {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = locale
        
        return formatter.number(from: string) != nil
    }
    
    static func isValidPercentage(_ string: String) -> Bool {
        let formatter = NumberFormatter()
        formatter.numberStyle = .percent
        
        return formatter.number(from: string) != nil
    }
}

日付バリデーション

日付フォーマットバリデーション

extension String {
    func isValidDate(format: String) -> Bool {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = format
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        
        return dateFormatter.date(from: self) != nil
    }
}

// 日付範囲のバリデーション
extension Date {
    func isBetween(_ startDate: Date, and endDate: Date) -> Bool {
        return self >= startDate && self <= endDate
    }
    
    func isInFuture() -> Bool {
        return self > Date()
    }
    
    func isInPast() -> Bool {
        return self < Date()
    }
}

URLバリデーション

extension String {
    func isValidURL() -> Bool {
        guard let url = URL(string: self) else { return false }
        
        return url.scheme != nil && url.host != nil
    }
    
    func isValidHTTPSURL() -> Bool {
        guard let url = URL(string: self) else { return false }
        
        return url.scheme == "https" && url.host != nil
    }
}

カスタムバリデーションの実装

バリデーションプロトコル

protocol Validatable {
    associatedtype ValidationError: Error
    func validate() throws
}

// エラー定義
enum ValidationError: LocalizedError {
    case invalidEmail
    case invalidPhoneNumber
    case invalidAge
    case invalidName
    case custom(String)
    
    var errorDescription: String? {
        switch self {
        case .invalidEmail:
            return "メールアドレスが無効です"
        case .invalidPhoneNumber:
            return "電話番号が無効です"
        case .invalidAge:
            return "年齢が無効です"
        case .invalidName:
            return "名前が無効です"
        case .custom(let message):
            return message
        }
    }
}

モデルへのバリデーション実装

struct User: Validatable {
    let name: String
    let email: String
    let age: Int
    let phoneNumber: String?
    
    func validate() throws {
        // 名前のバリデーション
        guard !name.isEmpty && name.count <= 50 else {
            throw ValidationError.invalidName
        }
        
        // メールアドレスのバリデーション
        guard email.isValidEmail() else {
            throw ValidationError.invalidEmail
        }
        
        // 年齢のバリデーション
        guard age.isInRange(0...150) else {
            throw ValidationError.invalidAge
        }
        
        // 電話番号のバリデーション(オプショナル)
        if let phone = phoneNumber, !phone.isEmpty {
            guard phone.isValidPhoneNumber() else {
                throw ValidationError.invalidPhoneNumber
            }
        }
    }
}

フォームバリデーション

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

import SwiftUI

struct RegistrationForm: View {
    @State private var email = ""
    @State private var password = ""
    @State private var emailError: String?
    @State private var passwordError: String?
    
    var body: some View {
        Form {
            Section {
                TextField("メールアドレス", text: $email)
                    .onChange(of: email) { newValue in
                        validateEmail(newValue)
                    }
                if let error = emailError {
                    Text(error)
                        .foregroundColor(.red)
                        .font(.caption)
                }
                
                SecureField("パスワード", text: $password)
                    .onChange(of: password) { newValue in
                        validatePassword(newValue)
                    }
                if let error = passwordError {
                    Text(error)
                        .foregroundColor(.red)
                        .font(.caption)
                }
            }
            
            Button("登録") {
                submitForm()
            }
            .disabled(!isFormValid)
        }
    }
    
    private var isFormValid: Bool {
        emailError == nil && passwordError == nil &&
        !email.isEmpty && !password.isEmpty
    }
    
    private func validateEmail(_ email: String) {
        if email.isEmpty {
            emailError = "メールアドレスを入力してください"
        } else if !email.isValidEmail() {
            emailError = "有効なメールアドレスを入力してください"
        } else {
            emailError = nil
        }
    }
    
    private func validatePassword(_ password: String) {
        if password.count < 8 {
            passwordError = "パスワードは8文字以上で入力してください"
        } else {
            passwordError = nil
        }
    }
    
    private func submitForm() {
        // フォーム送信処理
    }
}

高度なバリデーション技術

Combineを使用した非同期バリデーション

import Combine

class EmailValidator {
    private var cancellables = Set<AnyCancellable>()
    
    func validateEmailAvailability(_ email: String) -> AnyPublisher<Bool, Never> {
        // 実際のAPIコールをシミュレート
        Future<Bool, Never> { promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                // サーバーチェックのシミュレーション
                let unavailableEmails = ["[email protected]", "[email protected]"]
                promise(.success(!unavailableEmails.contains(email)))
            }
        }
        .eraseToAnyPublisher()
    }
}

バリデーションチェーンの実装

struct ValidationRule<T> {
    let validate: (T) -> Bool
    let errorMessage: String
}

class Validator<T> {
    private var rules: [ValidationRule<T>] = []
    
    func addRule(_ rule: ValidationRule<T>) -> Self {
        rules.append(rule)
        return self
    }
    
    func validate(_ value: T) -> Result<T, ValidationError> {
        for rule in rules {
            if !rule.validate(value) {
                return .failure(.custom(rule.errorMessage))
            }
        }
        return .success(value)
    }
}

// 使用例
let passwordValidator = Validator<String>()
    .addRule(ValidationRule(
        validate: { $0.count >= 8 },
        errorMessage: "パスワードは8文字以上必要です"
    ))
    .addRule(ValidationRule(
        validate: { $0.rangeOfCharacter(from: .uppercaseLetters) != nil },
        errorMessage: "大文字を1文字以上含めてください"
    ))
    .addRule(ValidationRule(
        validate: { $0.rangeOfCharacter(from: .decimalDigits) != nil },
        errorMessage: "数字を1文字以上含めてください"
    ))

国際化対応

ロケール対応のバリデーション

class LocalizedValidator {
    static func validatePostalCode(_ code: String, for countryCode: String) -> Bool {
        switch countryCode {
        case "JP":
            // 日本の郵便番号(XXX-XXXX)
            let regex = "^\\d{3}-\\d{4}$"
            return NSPredicate(format: "SELF MATCHES %@", regex)
                .evaluate(with: code)
        case "US":
            // アメリカの郵便番号(XXXXX or XXXXX-XXXX)
            let regex = "^\\d{5}(-\\d{4})?$"
            return NSPredicate(format: "SELF MATCHES %@", regex)
                .evaluate(with: code)
        default:
            return false
        }
    }
}

エラーハンドリング

詳細なエラー情報の提供

struct ValidationResult {
    let isValid: Bool
    let errors: [ValidationError]
    let warnings: [String]
    
    static func success() -> ValidationResult {
        return ValidationResult(isValid: true, errors: [], warnings: [])
    }
    
    static func failure(errors: [ValidationError], warnings: [String] = []) -> ValidationResult {
        return ValidationResult(isValid: false, errors: errors, warnings: warnings)
    }
}

class ComprehensiveValidator {
    func validateUser(_ user: User) -> ValidationResult {
        var errors: [ValidationError] = []
        var warnings: [String] = []
        
        // 各フィールドの検証
        if !user.email.isValidEmail() {
            errors.append(.invalidEmail)
        }
        
        if user.age < 13 {
            warnings.append("13歳未満のユーザーには保護者の同意が必要です")
        }
        
        return errors.isEmpty ? 
            .success() : 
            .failure(errors: errors, warnings: warnings)
    }
}

サードパーティライブラリとの比較

Foundationの組み込みバリデーション機能は、多くの基本的なニーズに対応できますが、より高度な機能が必要な場合は以下のようなサードパーティライブラリの使用を検討できます:

  • SwiftValidator: より宣言的なバリデーションルールの定義
  • ValidatedPropertyKit: プロパティラッパーを使用したバリデーション
  • Validator: 関数型プログラミングスタイルのバリデーション

Foundationを選択する利点:

  • 追加の依存関係が不要
  • Appleプラットフォームとの完全な互換性
  • 長期的なサポートとメンテナンス
  • 学習コストが低い

まとめ

Swift Foundationフレームワークは、iOS/macOSアプリケーション開発において必要十分なバリデーション機能を提供します。標準フレームワークの利点を活かしながら、カスタムバリデーションロジックを実装することで、堅牢なデータ検証システムを構築できます。