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