[Swift] 文字入力バリデーションを実装する(iOS)

## はじめに

TextField等の入力フォームで受け取った文字列をそのままサーバー側に投げるのは、サーバへの負担となってしまうのでなるべく避けたいところです。
そこで明らかに無効な入力があった場合、アプリ側で弾くことができるよう簡易的なバリデーション機能を設けることにしました。

## チェック対象

- Password

記事を分かりやすくするため、パスワードのバリデーションのみを考えることにします。

### 必要な要件

- 英数字のみ
- 大文字小文字の制限は設けない
- 文字数は6~8

よくある簡易的なパスワードという感じです。

## 実装

<img src="https://i.gyazo.com/42b03b12c4a26490833d815ed3c69a5e.png" width="400">

上記のようなフォームに入力した場合を想定してつくります。

### コード

```swift
import Foundation

// ①
enum ValidationResult {
    case valid
    case dataIsEmpty(section: String)
    case lengthInvalid(section: String, min: Int, max: Int)
    case invalidFormat(section: String)
    
    // ②
    var isValid: Bool {
        switch self {
        case .valid:
            return true
        case .dataIsEmpty:
            return false
        case .lengthInvalid:
            return false
        case .invalidFormat:
            return false
        }
    }
    
    // ③
    var errorMessage: String {
        switch self {
        case .valid:
            return ""
        case .dataIsEmpty(let section):
            return "\(section)の入力がありません"
        case .lengthInvalid(let section, let min, let max):
            return "\(section)：\(min)文字以上\(max)文字以下で入力してください"
        case .invalidFormat(let section):
            return "\(section)に使用できない文字が含まれています"
        }
    }
}

final class Validator {
    
    static let shared: Validator = .init()
    private init() {}
    
    // ④
    func passwordCheck(with _password: String?, min: Int, max: Int) -> ValidationResult {
        
        // ⑤
        guard let _password = _password, !_password.isEmpty else {
            return .dataIsEmpty(section: "パスワード")
        }
        
        // ⑥
        guard _password.count >= min && _password.count <= max else {
            return .lengthInvalid(section: "パスワード", min: min, max: max)
        }
        
        // ⑦
        let pattern = "[^a-zA-Z0-9]"
        if _password.range(of: pattern, options: .regularExpression) != nil {
            return .invalidFormat(section: "パスワード")
        }
        
        // ⑧
        return .valid
    }
    
}
```

### 解説

#### ① ValidationResult
enum `ValidationResult`を定義し、バリデーションの結果をenumで受け取ります。
caseはチェックする内容ごとに用意すると良いです。

- case valid (有効)
- case dataIsEmpty (空入力)
- case lengthInvalid (文字数)
- case invalidFormat (指定外の文字の使用)

今回はこの４つを用意しています。

バリデーションチェックの結果によってこのいずれかを返すことになります。

#### ② isValid: Bool

enumにBool型の`isValid`を持たせています。
これは好みによると思いますが、私は呼び出し側で`guard`や`if`などの条件分岐が書きやすくなるので用意しています。

#### ③ errorMessage: String

enumにString型の`errorMessage`を持たせています。
selfで条件分岐し、それぞれのケースに対応したStringを保持します。

呼び出し側は`UIAlertController`に`errorMessage`を渡して表示するなどして使用します。

#### ④ passwordCheck

この関数を外部から呼ぶことになります。
引数で`入力された文字列`と`文字数の最小値`、`文字数の最大値`を受け取ります。
今回はTextField.Textをそのまま受け取る想定ですのでString?型にしています。

処理した戻り値は事前に用意した`ValidationResult`です。

#### ⑤ 空文字の除外
受け取ったString?が`nil`もしくは`""`だった場合、`ValidationResult.dataIsEmpty`を返します。
その際、(section: "パスワード")とし、エラーメッセージに反映させたい文字列も一緒に渡しています。

(2022.06.30追記)nilチェックだけでなく、`!_password.isEmpty`を加えることで、`""`といった空文字もここで止めています。

このようにすることで、ユーザー名やメールアドレスといった他のバリデーションを行いたいときに、sectionの文字列を変更するだけで良くなります。


※2022.06.30追記
isEmptyのチェックは不要では？と編集リクエストをいただきました。
コメント欄にて返事をさせていただきました。


#### ⑥ 文字数チェック
受け取った文字列が、決められた文字数の範囲内かどうかを確認します。
範囲外の場合は`ValidationResult.lengthInvalid`を返します。
ここでも同じようにエラーメッセージに渡したい情報を用意しています。


#### ⑦ 不正な文字を使用していないかの確認
受け取った文字列に、使用できないものが含まれていないかの確認をします。

rangeメソッドを正規表現で使用し、nilで無かった場合(不正な文字が検出された場合)、
`ValidationResult. invalidFormat`を返します。

#### ⑧ 問題ない場合
`ValidationResult.valid`を返します。

## 呼び出し側

### コード

以下のような形で呼び出すことができます。

```swift
// フォームに入力された文字列と仮定
let textFieldText: String? = "dsna-uh/"

let result = Validator.shared.passwordCheck(with: textFieldText, min: 6, max: 8)

print(result.isValid) // false
print(result.errorMessage) // パスワードに使用できない文字が含まれています
```

isValidを使用してswitchで条件分岐させるような書き方もできます。

```swift
let result = Validator.shared.passwordCheck(with: textFieldText, min: 6, max: 8)

switch result.isValid {
case true:
    // 問題なかった場合の処理
case false:
    // 問題があった場合の処理
}
```

## 終わりに

ここまで読んでいただきありがとうございました。
試行錯誤しながら実装したものなので、良くない部分やもっとスマートな記述方法があったりするかもしれませんが、これからバリデーションを実装しようとしている方の参考になれば幸いです。

上述のコードはPlaygroundで動作しますので、ぜひお試しください。