Complex table view state changes made easy
During one of our estimation meetings, we were confronted with what we thought would be a cumbersome and costly challenge that could potentially result in Massive View Controllers.
The challenge was to create a login and registration form within one view controller without using the navigation stack. Everything should animate in one view with only the labels, buttons and textfields shifting positions. This prevents the user from losing any credentials that he or she already typed into one of the text fields.

Getting Started
While we were already thinking about a complex state machine that animates layout constraints back and forth and hides or shows views appropriately, we came up with an interesting idea; What if we used a UITableView and let its animation API do all the work for us. Everyone who looked into UITableViews a little deeper, discovered that they are actually quite flexible.
We started by separating the view into independent units like separators, text fields, buttons, etc. where each unit is a UITableViewCell. We also created a Swift Enumeration, declaring an identifier for each cell.
enum AuthCellType: String {
case Headline
case EmailTextField
case NameTextField
case PasswordTextField
case LoginButton
case Separator
}
Then we broke down every possible state of the view (e.g. login or register) into a list consisting of those units. To provide an example, here are two simplified versions of two distinct states:
let loginState = [
.Headline,
.Separator,
.EmailTextField,
.PasswordTextField,
.LoginButton
]let registerState = [
.EmailTextField,
.NameTextField,
.PasswordTextField,
.LoginButton
]
Animating state transitions
Then we thought about how to animate back and forth between the views. UITableView exposes an easy API for comfortably inserting and removing cells identified by an array of index paths:
func insertRowsAtIndexPaths(_ indexPaths: [NSIndexPath] withRowAnimation animation: UITableViewRowAnimation)func deleteRowsAtIndexPaths(_ indexPaths: [NSIndexPath], withRowAnimation animation: UITableViewRowAnimation)
So we had to come up with the NSIndexPaths of cells that would be added, and the index paths of cells that disappear on a transition between two view states.
Let’s assume the user starts on the login view and then finds out that he or she does not have an account yet and presses the button to register for a new account.
In this case the .Headline and .Separator at indexes 0 and 1 have to be removed and a new cell .NameTextField has to be added at index 1. The change set looks like this:
let addedIndexes = [1]
let removedIndexes = [0, 1]
Those change sets then have to be transformed to arrays of NSIndexPaths and would be passed to the previously mentioned UITableView methods to trigger the state change animation.
Automating change set generation
A naive solution would have been to specify arrays of NSIndexPaths of the inserted and deleted rows for every possible transition between view states. But we were looking for a more maintainable solution requiring less manual work.
Generating those change sets automatically is not a trivial problem. The key to the solution is the algorithm that was intended to solve the longest common subsequence problem (LCS). Luckily we found the great SwiftLCS library on github that can get you started with identifying the change set between two collections.
Our final solution based on SwiftLCS looks something like this:
func transitionToViewState(newState: AuthViewState) {
let diff = currentState.diff(newState)
tableView?.beginUpdates() tableView?.insertRowsAtIndexPaths(diff.addedIndexes,
withRowAnimation: .Fade) tableView?.deleteRowsAtIndexPaths(diff.removedIndexes,
withRowAnimation: .Fade) tableView?.endUpdates()
}
The transitionToViewState method can be called with an array of AuthCellType elements and the table view automatically animates to the respective state

Conclusion
This example surely does not fit for everyone. For us it was more of a reminder of how it can be useful to always rethink the possibilities of existing tools we can use with UIKit.
We would love to get in touch with you. Questions, feedback and any kind of comments are very welcome.
You can contact the Kitchen Stories iOS Team on Twitter and get the app here: