DEV Community

Diego Lavalle for Swift You and I

Posted on • Edited on • Originally published at swiftui.diegolavalle.com

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

Top comments (1)

Collapse
 
roblevintennis profile image
Rob Levin

I did almost the exact same form but with AgnosticUI + Vest. I find Vest really rocks for it's simple DSL (it looks exactly like any unit-testing framework you've ever used), it's modular and orthogonal so I can use it with anything, and it works for Svelte, React, Vue 3, and platform JavaScript (of course!) 💪 💯 🏆

I just made a demo too:
dev.to/roblevintennis/agnosticui-v...