iOSで簡易コーポレートサイト（アプリ）を作成する #2 -バリデーション編-

# 概要
Swift/SwiftUIで再現したウェブサイトのお問合せフォームに動的なバリデーションを実装していきます。

# バリデーション
バリデーションを作成するにあたって、ViewModelを作成して切り分けます。
「File > New > File..」を選択し、「Swift File」を選択して「ContactFormViewModel.swift」を作成します。
<img width="1796" alt="000001.jpg" src="https://dl.dropbox.com/s/vvgzlv3xpuru7bg/000001.jpg">
下記のように記述します。
PublishedはObservableObjectプロトコルに準拠したクラス内のプロパティを監視するのでクラスに継承させます。

```ContactFormViewModel.swift
import Foundation

class ContactFormViewModel:ObservableObject{
    var name = ""
    var email = ""
    var contactBody = ""
}
```

ContactFormView.swiftでContactFormViewModelから値を取得するように編集します。


```diff_swift:ContactFormView.swift
	import SwiftUI

	struct ContactFormView: View {
	    
+	    @ObservedObject var CFVM = ContactFormViewModel()

-	    @State var name = ""
-	    @State var email = ""
-       @State var contactBody = ""

	    var body: some View {
	        VStack{
	            Text("Contact").font(.title)
	                            
	            VStack{
	                Text("お問合わせフォーム").font(.headline).foregroundColor(.white)
	            }
	            .frame(maxWidth:.infinity)
	            .frame(height:100)
	            .background(.blue)
	            
	            VStack{
	                Text("お問合わせ内容を入力してください。").padding()
	                Group{
-	                    TextField("お名前",text:$name)
+	                    TextField("お名前",text:$CFVM.name)
	                        .keyboardType(.default)
	                        .textFieldStyle(RoundedBorderTextFieldStyle())
	                        .frame(maxWidth: .infinity)
	                        
	                    Text("お名前は必須です。")
	                        .multilineTextAlignment(.leading)
	                        .frame(maxWidth:.infinity,alignment: .leading)
	                        .padding(.leading, 10.0)
	                        .foregroundColor(.red)
	                        .font(.caption2)
	                    
	                }.padding(.bottom, 10.0)
	                
	                Group{
-	                    TextField("email",text:$email)
+	                    TextField("email",text:$CFVM.email)
	                        .keyboardType(.emailAddress)
	                        .textInputAutocapitalization(.never)
	                        .textFieldStyle(RoundedBorderTextFieldStyle())
	                        .frame(maxWidth: .infinity)
	                      
	                    Text("emailは必須かつemailの形式で入力してください。")
	                        .multilineTextAlignment(.leading)
	                        .frame(maxWidth:.infinity,alignment: .leading)
	                        .padding(.leading, 10.0)
	                        .foregroundColor(.red)
	                        .font(.caption2)
	                }.padding(.bottom, 10.0)
	                
	                Group{
-	                    TextField("問合わせ内容",text:$contactBody)
+	                    TextField("問合わせ内容",text:$CFVM.contactBody)
	                        .keyboardType(.emailAddress)
	                        .textFieldStyle(RoundedBorderTextFieldStyle())
	                        .frame(maxWidth: .infinity)
	                      
	                    Text("問合せ内容は必須かつ1文字以上10文字以内で入力して下さい。")
	                        .multilineTextAlignment(.leading)
	                        .frame(maxWidth:.infinity,alignment: .leading)
	                        .padding(.leading, 10.0)
	                        .foregroundColor(.red)
	                        .font(.caption2)
	                }.padding(.bottom, 10.0)
	                
	                Button(action:{
	                    
	                }){
	                    Text("送信")
	                        .frame(minWidth: 160)
	                        .foregroundColor(.white)
	                        .padding(12)
	                        .background(Color.accentColor)
	                        .cornerRadius(8)
	                }

	                Spacer()
	                    
	            }.frame(maxWidth:.infinity).frame(height:500)
	                            
	        }
	    }
	}

	struct ContactFormView_Previews: PreviewProvider {
	    static var previews: some View {
	        ContactFormView()
	    }
	}
```
## name必須バリデーション
ContactFormViewModel.swiftに記述していきます。
nameが空ならfalse、それ以外はtrueを返すfunctionを作成します。
nameを監視するためPublished属性をつけます。

```diff_swift:ContacFormViewModel.swift
    import Foundation

     class ContactFormViewModel:ObservableObject{
-       var name = ""
+       @Published  var name = ""
        var email = ""
        var contactBody = ""
        
+       //MARK: -Validation Function
+       func isNameValid() -> Bool{
+           return name != ""
+       }
+       
    }
```

```javascript:コピペ用
import Foundation

class ContactFormViewModel:ObservableObject{
    @Published  var name = ""
    var email = ""
    var contactBody = ""
    
    //MARK: -Validation Function
    func isNameValid() -> Bool{
        return name != ""
    }
}
```

次に、isCompleteという変数を設定します。
isCompleteはボタンを制御するのに使います。
作成したisNameValidがfalseだったらisCompleteという変数にfalse、それ以外はtrueを入れるようにします。

```diff_swift:ContactFormViewModel.swift
    import Foundation

    class ContactFormViewModel:ObservableObject{
        @Published var name = ""
        var email = ""
        var contactBody = ""
        
        //MARK: -Validation Founction
        func isNameValid() -> Bool{
            return name != ""
        }
        
+       var isComplete:Bool{
+           if !isNameValid() {
+               return false
+           }
+           return true
+       }

    }
```

```javascript:コピペ用
var isComplete:Bool{
    if !isNameValid() {
        return false
    }
    return true
}
```

次に、名前のエラーメッセージを設定します。

```diff_swift:ContactFormViewModel.swift
    import Foundation

    class ContactFormViewModel:ObservableObject{
        @Published var name = ""
        var email = ""
        var contactBody = ""
        
        //MARK: -Validation Founction
        func isNameValid() -> Bool{
            return name != ""
        }
        
        var isComplete:Bool{
            if !isNameValid() {
                return false
            }
            return true
        }
        
+       // MARK: - Validation Prompt
+       var namePrompt:String{
+           if isNameValid(){
+               return ""
+           }else{
+               return "お名前は必須です。"
+           }
+       }
    }

```

```javascript:コピペ用
// MARK: - Validation Prompt
var namePrompt:String{
    if isNameValid(){
        return ""
    }else{
        return "お名前は必須です。"
    }
}
```

ContactFormにViewに適用させてみます。
nameのエラーのテキストを変数に変えるのみです。

```diff_swift:ContactFormView.swift
    Group{
        TextField("氏名",text:$CFVM.name)
            .keyboardType(.default)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .frame(maxWidth: .infinity)

-       Text("お名前は必須です。")           
+       Text(CFVM.namePrompt)
            .multilineTextAlignment(.leading)
            .frame(maxWidth:.infinity,alignment: .leading)
            .padding(.leading, 10.0)
            .foregroundColor(.red)
            .font(.caption2)
        
    }
```
また、ボタンをバリデーションエラーの間は押せないようにします。
isCompleteがfalseの間は、opacityを0.5、disabledに設定します。

```diff_swift:ContactFormView.swift
    Button(action:{
        
    }){
        Text("送信")
            .frame(minWidth: 160)
            .foregroundColor(.white)
            .padding(12)
            .background(Color.accentColor)
            .cornerRadius(8)
    }
+   .opacity(CFVM.isComplete ? 1 : 0.5)
+   .disabled(!CFVM.isComplete)
```
```swift:コピペ用
.opacity(CFVM.isComplete ? 1 : 0.5)
.disabled(!CFVM.isComplete)
```
動的挙動を確認すると、氏名が未入力の間はエラーメッセージが出てボタンが押せず、入力すると動的にエラーメッセージが消えてボタンも押せるようになっていることを確認します。
> emailと問合せ内容はまだ静的にエラーメッセージが出ている状態です。
<img width="300" alt="000010.jpg" src="https://dl.dropbox.com/s/cst62vt966czfrb/000010.jpg">
<img width="300" alt="000020.jpg" src="https://dl.dropbox.com/s/3fzgz9wxr33ac90/000020.jpg">


## emailバリデーション
同様の要領でemailバリデーションを設定します。
バリデーションは正規表現を使います。

```diff_swift:ContactFormViewModel.swift
    import Foundation

    class ContactFormViewModel:ObservableObject{
        @Published var name = "" 
+       @Published  var email = ""
-       var email = ""
        var contactBody = ""
        
        //MARK: -Validation Function
        func isNameValid() -> Bool{
            return name != ""
        }
        
+       func isEmailValid() -> Bool{
+           let emailTest = NSPredicate(format:"SELF MATCHES %@","^[a-z0-9.]+@[a-z0-9.]+\\.[a-z]+$")
+           return emailTest.evaluate(with: email)
+       }
        
        var isComplete:Bool{
-           if !isNameValid() {
+           if !isNameValid() ||
+               !isEmailValid() {
                return false
            }
            return true
        }
        
        // MARK: - Validation Prompt           
        var namePrompt:String{
            if isNameValid(){
                return ""
            }else{
                return "氏名は必須です。"
            }
        }
        
+       var emailPrompt:String{
+           if isEmailValid(){
+               return ""
+           }else{
+               return "emailアドレスは必須かつemailの形式で入力してください。"
+           }
+       }
    }
```

```swift:コピペ用
import Foundation

class ContactFormViewModel:ObservableObject{
    
    @Published var name = ""
    @Published var email = ""
    var contactBody = ""
    
    //MARK: -Validation Function
    func isNameValid() -> Bool{

        return name != ""
    }
    
    func isEmailValid() -> Bool{
        let emailTest = NSPredicate(format:"SELF MATCHES %@","^[a-z0-9.]+@[a-z0-9.]+\\.[a-z]+$")
        return emailTest.evaluate(with: email)
    }
    
    var isComplete:Bool{
        if !isNameValid() ||
            !isEmailValid() {

            return false
        }
        return true
    }
    
    // MARK: - Validation Prompt
    var namePrompt:String{
        if isNameValid(){
            return ""
        }else{
            return "お名前は必須です。"
        }
    }
    
    var emailPrompt:String{
        if isEmailValid(){
            return ""
        }else{
            return "emailアドレスは必須かつemailの形式で入力してください。"
        }
    }
}
```

ContactFormViewのemailバリデーションメッセージ表示部分を編集します。

```diff_swift:ContactFormView.swift
    Group{
        TextField("email",text:$CFVM.email)
            .keyboardType(.emailAddress)
            .textInputAutocapitalization(.never)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .frame(maxWidth: .infinity)
          
-       Text("emailは必須かつemailの形式で入力してください。")
+       Text(CFVM.emailPrompt)
            .multilineTextAlignment(.leading)
            .frame(maxWidth:.infinity,alignment: .leading)
            .padding(.leading, 10.0)
            .foregroundColor(.red)
            .font(.caption2)
    }
```
バリデーションが同様に動くか確認します。

## Bodyバリデーション
同様に設定していきます。

```diff_swift:ContactFormViewModel.swift

    import Foundation

    class ContactFormViewModel:ObservableObject{
        @Published var name = ""
        @Published var email = ""
-       var contactBody = ""
+       @Published var contactBody = ""
        
        //MARK: -Validation Function
        func isNameValid() -> Bool{
            return name != ""
        }
        
        func isEmailValid() -> Bool{
            let emailTest = NSPredicate(format:"SELF MATCHES %@","^[a-z0-9.]+@[a-z0-9.]+\\.[a-z]+$")
            return emailTest.evaluate(with: email)
        }
        
+       func isContactBodyValid() -> Bool{
+           let contactBodyTest = NSPredicate(format:"SELF MATCHES %@","^.{1,10}$")
+           return contactBodyTest.evaluate(with: contactBody)
+       }
        
        var isComplete:Bool{
            if !isNameValid() ||
-               !isEmailValid(){
+               !isEmailValid() ||
+               !isContactBodyValid(){
                return false
            }
            return true
        }
        
        // MARK: - Validation Prompt
            
        var namePrompt:String{
            if isNameValid(){
                return ""
            }else{
                return "氏名は必須です。"
            }
        }
        
        var emailPrompt:String{
            if isEmailValid(){
                return ""
            }else{
                return "Emailアドレスは必須かつEmailの形式で入力してください。"
            }
        }
        
+       var contactBodyPrompt:String{
+           if isContactBodyValid(){
+               return ""
+           }else{
+               return "お問い合わせ内容は1文字以上10文字以下で入力してください。"
+           }
+       }
    }

```

```swift:コピペ用
import Foundation

class ContactFormViewModel:ObservableObject{
    
    @Published var name = ""
    @Published var email = ""
    @Published var contactBody = ""
    
    //MARK: -Validation Function
    func isNameValid() -> Bool{

        return name != ""
    }
    
    func isEmailValid() -> Bool{
        let emailTest = NSPredicate(format:"SELF MATCHES %@","^[a-z0-9.]+@[a-z0-9.]+\\.[a-z]+$")
        return emailTest.evaluate(with: email)
    }
    
    func isContactBodyValid() -> Bool{
        let contactBodyTest = NSPredicate(format:"SELF MATCHES %@","^.{1,10}$")
        return contactBodyTest.evaluate(with: contactBody)
    }
    
    var isComplete:Bool{
        if !isNameValid() ||
            !isEmailValid() ||
            !isContactBodyValid(){

            return false
        }
        return true
    }
    
    // MARK: - Validation Prompt
    var namePrompt:String{
        if isNameValid(){
            return ""
        }else{
            return "お名前は必須です。"
        }
    }
    
    var emailPrompt:String{
        if isEmailValid(){
            return ""
        }else{
            return "emailアドレスは必須かつemailの形式で入力してください。"
        }
    }
    
    var contactBodyPrompt:String{
        if isContactBodyValid(){
            return ""
        }else{
            return "お問い合わせ内容は1文字以上10文字以下で入力してください。"
        }
    }

}

```
ContactFormViewのbodyバリデーションメッセージ表示部分を編集します。

```diff_swift:ContactFormView.swift
    Group{
        TextField("問い合わせ",text:$CFVM.contactBody)
            .keyboardType(.emailAddress)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .frame(maxWidth: .infinity)

-       Text("問合せ内容は必須かつ1文字以上10文字以内で入力して下さい。")    
+       Text(CFVM.contactBodyPrompt)
            .multilineTextAlignment(.leading)
            .frame(maxWidth:.infinity,alignment: .leading)
            .padding(.leading, 10.0)
            .foregroundColor(.red)
            .font(.caption2)
    }
```

# 動作確認
エラーメッセージとボタンの色が動的に表示されたり消えたりすることを確認します。
<img width="300" alt="000030.jpg" src="https://dl.dropbox.com/s/29j64z600hrtw33/000030.jpg">
<img width="300" alt="000040.jpg" src="https://dl.dropbox.com/s/wk78gqcxuwuod5b/000040.jpg">
<img width="300" alt="000050.jpg" src="https://dl.dropbox.com/s/0gicntdkme4awkq/000050.jpg">

バリデーションが完成しました。


# 最終的なコード
## ContactFormViewModel

```swift:ContactFormViewModel
import Foundation

class ContactFormViewModel:ObservableObject{
    
    @Published var name = ""
    @Published var email = ""
    @Published var contactBody = ""
    
    //MARK: -Validation Function
    func isNameValid() -> Bool{

        return name != ""
    }
    
    func isEmailValid() -> Bool{
        let emailTest = NSPredicate(format:"SELF MATCHES %@","^[a-z0-9.]+@[a-z0-9.]+\\.[a-z]+$")
        return emailTest.evaluate(with: email)
    }
    
    func isContactBodyValid() -> Bool{
        let contactBodyTest = NSPredicate(format:"SELF MATCHES %@","^.{1,10}$")
        return contactBodyTest.evaluate(with: contactBody)
    }
    
    var isComplete:Bool{
        if !isNameValid() ||
            !isEmailValid() ||
            !isContactBodyValid(){

            return false
        }
        return true
    }
    
    // MARK: - Validation Prompt
    var namePrompt:String{
        if isNameValid(){
            return ""
        }else{
            return "お名前は必須です。"
        }
    }
    
    var emailPrompt:String{
        if isEmailValid(){
            return ""
        }else{
            return "emailアドレスは必須かつemailの形式で入力してください。"
        }
    }
    
    var contactBodyPrompt:String{
        if isContactBodyValid(){
            return ""
        }else{
            return "お問い合わせ内容は1文字以上10文字以下で入力してください。"
        }
    }

}

```

## ContactFormView

```swift:ContactFormView
import SwiftUI

struct ContactFormView: View {
    @ObservedObject var CFVM = ContactFormViewModel()
    
    var body: some View {
        VStack{
            Text("Contact").font(.title)
                            
            VStack{
                Text("お問合わせフォーム").font(.headline).foregroundColor(.white)
            }
            .frame(maxWidth:.infinity)
            .frame(height:100)
            .background(.blue)
            
            VStack{
                Text("お問合わせ内容を入力してください。").padding()
                Group{
                    TextField("お名前",text:$CFVM.name)
                        .keyboardType(.default)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .frame(maxWidth: .infinity)
                        
                    Text(CFVM.namePrompt)
                        .multilineTextAlignment(.leading)
                        .frame(maxWidth:.infinity,alignment: .leading)
                        .padding(.leading, 10.0)
                        .foregroundColor(.red)
                        .font(.caption2)
                    
                }.padding(.bottom, 10.0)
                
                Group{
                    TextField("email",text:$CFVM.email)
                        .keyboardType(.emailAddress)
                        .textInputAutocapitalization(.never)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .frame(maxWidth: .infinity)
                      
                    Text(CFVM.emailPrompt)
                        .multilineTextAlignment(.leading)
                        .frame(maxWidth:.infinity,alignment: .leading)
                        .padding(.leading, 10.0)
                        .foregroundColor(.red)
                        .font(.caption2)
                }.padding(.bottom, 10.0)
                
                Group{
                    TextField("問合わせ内容",text:$CFVM.contactBody)
                        .keyboardType(.emailAddress)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .frame(maxWidth: .infinity)
                      
                    Text(CFVM.contactBodyPrompt)
                        .multilineTextAlignment(.leading)
                        .frame(maxWidth:.infinity,alignment: .leading)
                        .padding(.leading, 10.0)
                        .foregroundColor(.red)
                        .font(.caption2)
                }.padding(.bottom, 10.0)
                
                Button(action:{
                      
                }){
                    Text("送信")
                        .frame(minWidth: 160)
                        .foregroundColor(.white)
                        .padding(12)
                        .background(Color.accentColor)
                        .cornerRadius(8)
                }
                .opacity(CFVM.isComplete ? 1 : 0.5)
                .disabled(!CFVM.isComplete)

                Spacer()
                    
            }.frame(maxWidth:.infinity).frame(height:500)
                            
        }
    }
}

struct ContactFormView_Previews: PreviewProvider {
    static var previews: some View {
        ContactFormView()
    }
}
```

# 関連コンテンツ
* 次：[iOSで簡易コーポレートサイト（アプリ）を作成する #3 -API連携編-](https://qiita.com/minimumskills/private/7f63624bf13cb3a5b2c1)
* 前：[iOSで簡易コーポレートサイト（アプリ）を作成する #1 -TOP画面とContact画面編-](https://qiita.com/minimumskills/private/748b65ce389a905dcc3e)
* [超シンプルなWeb（アプリ）作成を通して各種フロントエンド技術を体験する - 2023 Early -](https://qiita.com/minimumskills/private/9e7efe86e244bb9844b0)