概要
Swift/SwiftUIで再現したウェブサイトのお問合せフォームに動的なバリデーションを実装していきます。
バリデーション
バリデーションを作成するにあたって、ViewModelを作成して切り分けます。
「File > New > File..」を選択し、「Swift File」を選択して「ContactFormViewModel.swift」を作成します。
下記のように記述します。
PublishedはObservableObjectプロトコルに準拠したクラス内のプロパティを監視するのでクラスに継承させます。
import Foundation
class ContactFormViewModel:ObservableObject{
var name = ""
var email = ""
var contactBody = ""
}
ContactFormView.swiftでContactFormViewModelから値を取得するように編集します。
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属性をつけます。
import Foundation
class ContactFormViewModel:ObservableObject{
- var name = ""
+ @Published var name = ""
var email = ""
var contactBody = ""
+ //MARK: -Validation Function
+ func isNameValid() -> Bool{
+ return name != ""
+ }
+
}
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を入れるようにします。
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
+ }
}
var isComplete:Bool{
if !isNameValid() {
return false
}
return true
}
次に、名前のエラーメッセージを設定します。
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 "お名前は必須です。"
+ }
+ }
}
// MARK: - Validation Prompt
var namePrompt:String{
if isNameValid(){
return ""
}else{
return "お名前は必須です。"
}
}
ContactFormにViewに適用させてみます。
nameのエラーのテキストを変数に変えるのみです。
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に設定します。
Button(action:{
}){
Text("送信")
.frame(minWidth: 160)
.foregroundColor(.white)
.padding(12)
.background(Color.accentColor)
.cornerRadius(8)
}
+ .opacity(CFVM.isComplete ? 1 : 0.5)
+ .disabled(!CFVM.isComplete)
.opacity(CFVM.isComplete ? 1 : 0.5)
.disabled(!CFVM.isComplete)
動的挙動を確認すると、氏名が未入力の間はエラーメッセージが出てボタンが押せず、入力すると動的にエラーメッセージが消えてボタンも押せるようになっていることを確認します。
emailと問合せ内容はまだ静的にエラーメッセージが出ている状態です。
emailバリデーション
同様の要領でemailバリデーションを設定します。
バリデーションは正規表現を使います。
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の形式で入力してください。"
+ }
+ }
}
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バリデーションメッセージ表示部分を編集します。
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バリデーション
同様に設定していきます。
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文字以下で入力してください。"
+ }
+ }
}
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バリデーションメッセージ表示部分を編集します。
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)
}
動作確認
エラーメッセージとボタンの色が動的に表示されたり消えたりすることを確認します。
バリデーションが完成しました。
最終的なコード
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
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()
}
}