Manage UITextField with Combine framework and other techniques

Manage UITextField with Combine framework and other techniques

Introduction:

In this Tutorial, you’ll learn how to manage UITextFields changes using the Combine framework.

Currently, there are many ways to detect the changes of UITextFields:

  1. Delegate protocol
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    //do stuff
    return true
}
  1. Selectors
textField.addTarget(self, action: #selector(ViewController.textFieldDidChange(_:)), for: .editingChanged)
...
@objc func textFieldDidChange(_ textField: UITextField) {
    // do stuff
}

We will use features of Combine framework. We assume that you know about publishers, subscriptions and operators of Combine.

The Challenge:

The challenge is to validate the email format and the strength of the password to enable the submit button such as the following image below:

alt text

The Solution:

We need to know how to detect when the UITextField changes. To achieve this goal, we will use the target-action pattern with the Combine framework. Also, to enable the submit button we need to check these two things are done:

  • The email must be a valid format
  • The password must be strong

To start, our view controller looked like this:

// ViewController.swift
import UIKit

class ViewController: UIViewController {
 
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var submitButton: UIButton!
    ...
}

First, we need to detect the changes of the UITextFields. For this, we can use a target-action pattern like the code below:

@IBAction private func emailChanged(_ sender: UITextField) {
    email = sender.text ?? ""
}
    
@IBAction private func passwordChanged(_ sender: UITextField) {
    password = sender.text ?? ""
}

This is only for text fields with outlets, when we are working with Swift UI, every control is linked with the corresponding publisher.

Second, create some variables to manage the state of our inputs. For this, publishers of Combine framework is used.

@Published var email = ""
@Published var password = ""

@Published is one of the most useful property wrappers that allows us to create observable objects that automatically announce when changes occur.

Validating the Email

To validate the email, we can implement a validation to make sure the user entered email has a correct format. I’m using a small String extension.

var isEmailValidPublisher: AnyPublisher<Bool, Never> {
    return $email
    .receive(on: RunLoop.main)
    .map { email in
        let band = email.isFormatValidEmail
        return band
    }
    .eraseToAnyPublisher()
}

Validating the email through an extension is very easy. However, we return an AnyPublisher<Bool, Never>. This is because, later we will combine multiple publishers into a multi-stage chain before we subscribe to the final result (true or false).

Validating the Password

Third-party libraries like Navajo-Swift can be used to validate the strongest of passwords, but for this tutorial we are using an extension and return an AnyPublisher<Bool, Never> similar to the Email Validation.

var isPasswordValidPublisher: AnyPublisher<Bool, Never> {
    return $password
    .receive(on: RunLoop.main)
    .map { password in
        let band = password.isStrongPassword
        return band
    }
    .eraseToAnyPublisher()
}

Putting it All Together

To get the final result we need to join the Email validation and Password validation to decide whether to enable the submit button or not. We need to implement a final form validation.

var isFormValidPublisher: AnyPublisher<Bool, Never> {
    return Publishers.CombineLatest(isEmailValidPublisher, isPasswordValidPublisher)
        .map { validatedEmail, validatePassword in
            return (validatedEmail &&  validatePassword)
    }
    .eraseToAnyPublisher()
}

Finally we need to update the submit button according to the result of the isFormValidPublisher. Essentially, we need to create the subscription to receive this result from our publisher and set the state of the submit button.

private var stream: AnyCancellable?
 
override func viewDidLoad() {
    super.viewDidLoad()
    stream = isFormValidPublisher
        .receive(on: RunLoop.main).assign(to: \.isEnabled, on: submitButton)
}

In case you’re wondering why we need stream as AnyCancellable and RunLoop.main. We need this property called stream to keep a reference to the subscription, so it doesn’t get cancelled until the view controller goes away. Also, our code interfaces with the UI, need to run on the UI thread. We can tell it to execute this code on the UI thread by calling .receive(on: RunLoop.main).

For a review of the full source code, please checkout my git repository of this tutorial.

comments powered by Disqus