Encoded Reflections: Code + Design Theory

Designing a great user interface starts off with the best of intentions; make it pleasing to the eye, providing great feedback, and ensuring a delightful interaction. However, when we get to the code it’s often a game of tacking on functionality, extra validation rules, and an ever growing management responsibility. Recently, I’ve been diving into Human-Centered Design, and how it can be applied in a technical context. So in light of LinkedIn now supporting code snippets I thought it’d be a great time to translate theoretical concepts to a real-world example – form pages.

Forms are tricky because there’s usually a lot going on, even in the simplest ones. But the central questions are basically the same in whatever language or platform you’re developing with: How do you let your users know what they CAN do, what they SHOULD do, and what they CAN’T do?

The most interesting thing I’ve found is that the more I think in terms of good design, the more readable my code becomes. This has nothing to do with design patterns or software architecture in particular, but more with writing code that uses language and concepts from usability design; in essence designing the code as a product with the programmer (in this case me, but it could be others in a large team) as the end user.

The example in this post is in Swift 3, and is only a small implementation of design theory as I see it applying to form field validation. First off, let’s get a bit of state management going, and fold in a signifier protocol; then we can talk about how theory is driving the code implementation.

//1
enum FormState{
    case Editable, Disabled, Loading, Interrupted, Complete
}

//2
protocol Signify{
    var currentState: FormState { get set }
    
//3
    func configureSignifiers()
    func updateSignifierState(to newState: FormState)
}

//4
extension Signify{
    mutating func updateSignifierState(to newState: FormState) {
        currentState = newState
        configureSignifiers()
    }
}

  1. First, we declare a simple enum to keep track of all the possible states our form can be in.
  2. Then we declare a protocol that will deal exclusively with how we designate and update the signifiers present in our forms.
  3. In here are two methods that all forms will have to conform too; the first is where you set all configurations for the given states, the second a wrapper for updating the state logic.
  4. Finally, you can declare a default implementation as a protocol extension, or you could alternatively declare it’s implementation directly in the form (as we would do with the configureSignifiers() method).

On first glance this might not seem like anything super cool, but I would draw your attention to what we’ve just done. The FormState enum is now a base definition for the physical constraints we want to impose on the form. On top of that, defining a Complete state means we’ve introduced an forcing/interlocking constraint in the form; it’s now very easy to keep the user from submitting the form while it is incomplete, all that’s needed is only enable the submit button when the form state is marked as Complete.

We can go even further. If we provide a set of good feedback systems for the user, as well as for ourselves as debuggers, we get a great level of usability. For instance, if we wanted to flesh out a custom response type that could be used to help the user understand how to fill out the form, and what they might be doing wrong, while at the same time providing great information for stepping through code, we could.

//1
enum FeedbackResult {
    case TooShort(forContext: String)
    case MissingNumber(forContext: String)
    case MissingSpecial(forContext: String)
    case DontMatch(forContext: String)
    case Valid
   
//2 
    typealias FeedbackTuple = (message: String, valid: Bool)
    
//3
    var info: FeedbackTuple {
        switch self {
            case .TooShort(let context):
                return (message: "Your \(context) is too short", valid: false)
            case .MissingNumber(let context):
                return (message: "Your \(context) needs a number", valid: false)
            case .MissingSpecial(let context):
                return (message: "Your \(context) needs a special character", valid: false)
            case .DontMatch(let context):
                return (message: "Your \(context)'s don't match", valid: false)
            case .Valid:
                return (message: "Go ahead and log in", valid: true)
        }
    }
}
  1. We’ve defined another enum, this time that deals exclusively with form validation feedback. The case list can be easily expanded and managed.
  2. This is a simple type alias declaration of a custom type we want to be able to access. This is where we’re constructing a type that is useful to the user and to our future selves.
  3. The info property is a computed property on the enum, letting us get a custom message and a valid boolean for each case. If we paired this with reactive programming, or simply used the UITextField delegate method that fires when the text is changed (ie. when a user inputs keystrokes), we’ve got a real-time validation system.

But so far, these two enums are not used in any concrete way. What we could then add would be another protocol, this one for string validation using our enums and extending the NSString class for easy access.

//1
protocol Validateable {
    func isValidEmail() -> FeedbackResult
    func isValidPassword() -> FeedbackResult
}

//2
extension String: Validateable {

//3
    func isValidEmail() -> FeedbackResult {
        let context = "email"
        
        if !self.contains("@") {
            return FeedbackResult.MissingSpecial(forContext: context)
        }
        
        return FeedbackResult.Valid
    }
    
    func isValidPassword() -> FeedbackResult {
        let context = "password"
        
        if self.characters.count < 5 {
            return FeedbackResult.TooShort(forContext: context)
        }
        
        return FeedbackResult.Valid
    }
}
  1. A small protocol that evaluates whether a given field input is a valid email or password
  2. Extending the NSString class to conform to this protocol.
  3. Writing the actual implementation of the email and password validation rules. You could use regex here or regular expressions instead of simple string checks.

We’ve now got the building blocks of a complex validation system, complete with feedback for users and developers! There are always ways to make code better, and this example is no exception. But what I’m most excited about is that attempting to translate physical design theory into code not only helped me manage my application better, it made my code easier for me to understand (not to mention helped solidify the actual conceptual learning).

It turns out theory can be quite concrete.

Leave a comment