Most of the logic of this view controller is in the validateInputs method. For example, to see if the new password is too short, we have the following if statement:
| if newPasswordTextField.text?.count ?? 0 < 6 { |
Because UITextField’s text property is optional (due to legacy Objective-C code), this statement is extra fiddly. If the view model could express the content of each text field, we could separate this logic from UIKit.
So far, the interaction between the view model and the views has been one-way. Any changes to the view model cause the didSet observer to execute. This is one-way data binding: the views are bound to the view model.
We can expand this to two-way data binding, where changes to the views update values in the view model. To do this in a thorough fashion, we could have each UITextField copy user input to the view model. We’d need this if user input enabled or disabled the Submit button on each keystroke. But that’s not how this view controller behaves.
We’re not checking text field content until the user taps the Submit button. So we can use a simpler approach of copying the fields to the view model on-demand. Let’s start by adding a property for each text field, showing what the user entered. Set their initial values to empty strings:
| var oldPassword = "" |
| var newPassword = "" |
| var confirmPassword = "" |
Back in ChangePasswordViewController, add the following method to copy the text property from each UITextField to the view model:
| private func updateViewModelToTextFields() { |
| viewModel.oldPassword = oldPasswordTextField.text ?? "" |
| viewModel.newPassword = newPasswordTextField.text ?? "" |
| viewModel.confirmPassword = confirmPasswordTextField.text ?? "" |
| } |
Add a line to the top of changePassword that calls this new method:
| @IBAction private func changePassword() { |
» | updateViewModelToTextFields() |
| guard validateInputs() else { |
| return |
| } |
| setUpWaitingAppearance() |
| attemptToChangePassword() |
| } |
By applying nil-coalescing, the view model can hold regular strings instead of optional strings. This will simplify things as we move the conditionals into the view model. Start from the first conditional in validateInputs.
| if oldPasswordTextField.text?.isEmpty ?? true { |
Define a new computed variable in the view model. Copy everything before the nil-coalescing operator ?? into the body of the computed variable.
| var isOldPasswordEmpty: Bool { oldPassword.isEmpty } |
Make sure there are no compiler errors. Then change the conditional in validateInputs to use this computed property instead:
| if viewModel.isOldPasswordEmpty { |
Run tests to make sure everything is still fine. Then repeat these steps to define the following computed properties in ChangePasswordViewModel, changing the conditionals to call them:
The resulting validateInputs method is cleaner, expressing things at a slightly higher level of abstraction:
| private func validateInputs() -> Bool { |
| if viewModel.isOldPasswordEmpty { |
| viewModel.inputFocus = .oldPassword |
| return false |
| } |
| |
| if viewModel.isNewPasswordEmpty { |
| showAlert(message: viewModel.enterNewPasswordMessage) { |
| [weak self] _ in |
| self?.viewModel.inputFocus = .newPassword |
| } |
| return false |
| } |
| |
| if viewModel.isNewPasswordTooShort { |
| showAlert(message: viewModel.newPasswordTooShortMessage) { |
| [weak self] _ in |
| self?.resetNewPasswords() |
| } |
| return false |
| } |
| |
| if viewModel.isConfirmPasswordMismatched { |
| showAlert( |
| message: viewModel.confirmationPasswordDoesNotMatchMessage) { |
| [weak self] _ in |
| self?.resetNewPasswords() |
| } |
| return false |
| } |
| |
| return true |
| } |
You should notice a few things:
The conditionals are now expressed as ideas, without testing whether any UIKit properties are nil.
The alert messages are now expressed as ideas, not as string literals.
Changing input focus is now expressed as ideas, not as UIKit commands.
Business rules like “What’s the minimum number of characters we require in a new password?” now live in the view model. Pulling such rules out of the view controller makes it easier to see them in one place. They’re no longer scattered around code that’s manipulating views.
There’s also some nil-coalescing in attemptToChangePassword. Since it’s called after changePassword updates the view model, we can replace them with references to the view model:
| private func attemptToChangePassword() { |
| passwordChanger.change( |
| securityToken: securityToken, |
» | oldPassword: viewModel.oldPassword, |
» | newPassword: viewModel.newPassword, |
| onSuccess: { [weak self] in |
| self?.handleSuccess() |
| }, |
| onFailure: { [weak self] message in |
| self?.handleFailure(message) |
| }) |
| } |
If you start with MVVM, it’s easier to write tests directly against the view model. But if you have a solid suite of tests on a view controller, you can adopt MVVM later. And you can do so in safe and gradual steps using disciplined refactoring.