iOSアプリのバリデーションについて考えてみた（Swift）


![Xcode-12](https://img.shields.io/badge/Xcode-12-brightgreen) ![Swift-5.3](https://img.shields.io/badge/Swift-5.3-brightgreen) ![iOS-14](https://img.shields.io/badge/iOS-14-brightgreen)

## はじめに
iOS アプリを作る際たま〜〜〜〜〜にクライアント側でバリデーションしてよ！って言われて実装することがあります。ただ下記のようにするのは嫌なんでなにかしらクラスを分けたくて考えてみました。どなたかいい案があればご教授いただけると幸いです:pray:

```swift:ViewController.swift
@IBAction private func register(_ sender: Any) {
    let value = textField.text!
    var message = ""
    if value.isEmpty {
        message = "空だよ！"
    } else if value.count > 7 {
        message = "7文字以内にしてよ！"
    }
    if !message.isEmpty {
        showAlert(message: message)
    }
}
```

ライブラリもあるみたいですが archived:bangbang:
[adamwaite/Validator](https://github.com/adamwaite/Validator)

## 必要そうな機能
バリデーションで必要そうな機能は下記かなと思います:thinking:

* 値の受け取り  
`String`, `Int`, `Date`　とか色々型を受けられた方がいいかも？
* 成功・失敗の返却
* 複数条件の設定
* エラーメッセージの返却  
エラーを返すでもいいかも？

## 実装
実装はほぼこちらの記事を参考にさせていただきました:pray:

[【iOS】アプリクライアント側での文字入力のValidation機構について（UITextField, UITextViewなど)](https://iganin.hatenablog.com/entry/2019/09/23/171221)

```swift
protocol ValidationError: Swift.Error {
}

enum ValidationResult {
    case valid
    case invalid(ValidationError)
}

extension ValidationResult {
    var isValid: Bool {
        switch self {
        case .valid:
            return true
        case .invalid:
            return false
        }
    }
}

protocol Validator {
    func validate() -> ValidationResult
}

protocol CompositeValidator: Validator {
    var validators: [Validator] { get }
}

extension CompositeValidator {
    func validate() -> ValidationResult {
        guard let result = validators.map({ $0.validate() }).first(where: { !$0.isValid }) else {
            return .valid
        }
        return result
    }
}

enum NameValidationError: ValidationError {
    case empty
    case length
}

extension NameValidationError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .empty:
            return "名前を入力してください。"
        case .length:
            return "名前は7文字以内で入力してください。"
        }
    }
}

struct NameEmptyValidator: Validator {
    let name: String
    func validate() -> ValidationResult {
        if name.isEmpty {
            return .invalid(NameValidationError.empty)
        }
        return .valid
    }
}

struct NameLengthValidator: Validator {
    let name: String
    func validate() -> ValidationResult {
        if name.count > 7 {
            return .invalid(NameValidationError.length)
        }
        return .valid
    }
}

enum BirthdayValidationError: ValidationError {
    case minimum
}

extension BirthdayValidationError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .minimum:
            return "18歳未満の方はご遠慮ください🙏"
        }
    }
}

struct BirthdayRangeValidator: Validator {
    let birthday: Date
    func validate() -> ValidationResult {
        let calendar = Calendar.current
        let age = calendar.dateComponents([.year], from: birthday, to: Date()).year!
        if age < 18 {
            return .invalid(BirthdayValidationError.minimum)
        }
        return .valid
    }
}

struct ProfileValidator: CompositeValidator {
    let validators: [Validator]
    init(name: String, birthday: Date) {
        self.validators = [
            NameEmptyValidator(name: name),
            NameLengthValidator(name: name),
            BirthdayRangeValidator(birthday: birthday)
        ]
    }
}
```

使い方

```swift
let validator = ProfileValidator(name: nameTextField.text!, 
                                 birthday: dateFormatter.date(from: birthdayTextField.text!)!)
switch validator.validate() {
case .valid:
  print("問題なし！")
case .invalid(let error):
  print(error.localizedDescription)
}
```

`validate()` の中でごちゃごちゃするの嫌だったので１つずつ分けてみました。
`Validator` にプロパティを１つしか持たせていませんがいくらでも追加は可能です！（開始日・終了日とか）

呼び出し側で文言変えられるように `Error` を返してみたんですがキャストしないと使えないし微妙かも。。。

実際の動作はこんな感じ。

![validation](https://user-images.githubusercontent.com/34936885/107130663-376b3500-6913-11eb-821f-afd245b5d790.gif)

## おわりに
必要そうな機能はとりあえず満たしているんじゃないかなと思います:v:
Android の実装と差がでるの嫌やからバリデーションはサーバーだけでやってくれって思う:innocent:

もっといい方法あるよ！という方がいればご教授いただけると幸いです:bow:

## 参考
* [【iOS】アプリクライアント側での文字入力のValidation機構について（UITextField, UITextViewなど)](https://iganin.hatenablog.com/entry/2019/09/23/171221)
* [iOSアプリのバリデーションについて考えてみた](https://blog.mothule.com/ios/swift/ios-swift-validation)