Form Validation with Combine

diegolavalle

Diego Lavalle

Posted on January 20, 2020

Form Validation with Combine

In WWDC 2019 session Combine in Practice we learned how to apply the Combine Framework to perform validation on a basic sign up form built with UIKit. Now we want to apply the same solution to the SwiftUI version of that form which requires some adaptation.

Form validation with Combine

We begin by declaring a simple form model separate from the view…

class SignUpFormModel: ObservableObject {
  @Published var username: String = ""
  @Published var password: String = ""
  @Published var passwordAgain: String = ""
}

And link each property to the corresponding TextField control…

struct SignUpForm: View {

  @ObservedObject var model = SignUpFormModel()

  var body: some View {
    
    TextField("Username", text: $model.username)
    
    TextField("Password 'secreto'", text: $model.password)
    
    TextField("Password again", text: $model.passwordAgain)
    

Now we can begin declaring the publishers in our SignUpFormModel. First we want to make sure the password has mor than six characters, and that it matches the confirmation field. For simplicity we will not use an error type, we will instead return invalid when the criteria is not met…

var validatedPassword: AnyPublisher<String?, Never> {
  $password.combineLatest($passwordAgain) { password, passwordAgain in
    guard password == passwordAgain, password.count > 6 else {
      return "invalid"
    }
    return password
  }
  .map { $0 == "password" ? "invalid" : $0 }
  .eraseToAnyPublisher()
}

For the user name we want to simultate an asynchronous network request that checks whether the chosen moniker is already taken…

func usernameAvailable(_ username: String, completion: @escaping (Bool) -> ()) -> () {
  DispatchQueue.main .async {
    if (username == "foobar") {
      completion(true)
    } else {
      completion(false)
    }
  }
}

As you can see, the only available name in our fake server is foobar.

We don't want to hit our API every second the user types into the name field, so we leverage debounce() to avoid this…

var validatedUsername: AnyPublisher<String?, Never> {
  return $username
    .debounce(for: 0.5, scheduler: RunLoop.main)
    .removeDuplicates()
    .flatMap { username in
      return Future { promise in
        usernameAvailable(username) { available in
          promise(.success(available ? username : nil))
        }
      }
  }
  .eraseToAnyPublisher()
}

Now to make use of this publisher we need some kind of indicator next to the text box to tell us whether we are making an acceptable choice. The indicator should be backed by a private @State variable in the view and outside the model.

To connect the indicator to the model's publisher we leverage the onReceive() modifier. On the completion block we manually update the form's current state…

Text(usernameAvailable ? "✅" : "❌")
.onReceive(model.validatedUsername) {
  self.usernameAvailable = $0 != nil
}

An analog indicator can be declared for the password fields.

Finally, we want to combine our two publishers to create an overall validation of the form. For this we create a new publisher…

var validatedCredentials: AnyPublisher<(String, String)?, Never> {
  validatedUsername.combineLatest(validatedPassword) { username, password in
    guard let uname = username, let pwd = password else { return nil }
    return (uname, pwd)
  }
  .eraseToAnyPublisher()
}

We can then hook this validation directly into our Sign Up button and its disabled state.

  Button("Sign up") {  }
  .disabled(signUpDisabled)
  .onReceive(model.validatedCredentials) {
    guard let credentials = $0 else {
      self.signUpDisabled = true
      return
    }
    let (validUsername, validPassword) = credentials
    guard validUsername != nil  else {
      self.signUpDisabled = true
      return
    }
    guard validPassword != "invalid"  else {
      self.signUpDisabled = true
      return
    }
    self.signUpDisabled = false
  }
}

Check out the associated Working Example to see this technique in action.

FEATURED EXAMPLE: Fake Signup - Validate your new credentials

💖 💪 🙅 🚩
diegolavalle
Diego Lavalle

Posted on January 20, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Codable observable objects
ios Codable observable objects

August 1, 2020

Form Validation with Combine
ios Form Validation with Combine

January 20, 2020

Flip clock in SwiftUI
ios Flip clock in SwiftUI

October 22, 2019

Keyboard-aware views
ios Keyboard-aware views

August 2, 2020