Tyten

View Models are not Abstractions

A little background first

Before I go into my approach and goals/non-goals for this design system, I want to tread over some patterns that I've used at various stages through my career

Who should read this part

If you haven't been doing iOS development for a long period of time, or just want to retread some patterns that don't lend themselves to reuse.

Who can skip over this

Anyone familiar with building UIKit based interfaces via code.

In the beginning

When I was first learning iOS development my UI code was very tightly coupled with my data layer. Usually something along the following lines inside of a UIViewController subclass:

class ModelObject {
    let title: String
    var image: Image?
}
class ExampleViewController: UIViewController {
    let label = UILabel()
    let imageView = UIImageView()
    
    var modelObject: ModelObject
    
    override func viewDidLoad() {
        // Assume some code exists to set up the view before here.
        titleLabel = modelObject.foo
        imageView.image = modelObject.bar
    }
}

At the most basic level this seems nice. You can read the line and immediately know what data from what source is going to what view element. It literally couldn't be clearer and exists all within one scope.

This starts to suck very rapidly, however. Add some more depth to your app and you're going to find yourself writing one-off implmentations of views all over the place. If your experience resembles mine, you'll find a lot of inconsistinces in implementation across different time periods and/or different engineers, with all of the differences adding up to an impossible task of maintaining design consistency and updatability.

View Models

Imagine, if you will, a world where the view didn't know about the model object, but instead a herald that took your data, formatted it for you, and presented the data as you want it for a particular view.

// In this implementation the model object never directly
// interacts with the view.
class ModelObject {
    let title: String
    var image: Image?
}
// The view model instead formats the data and provides it
// to the view.
struct ModelObjectViewModel {
    let title: String
    let image: Image
    
    static func viewModel(from modelObject: ModelObject) -> ModelObjectViewModel {
        let newViewModel = ModelObjectViewModel()
    }
    
    init(modelObject: ModelObject) {
        self.title = formatTitle(modelObject.title)
        self.image = modelObject.image ?? UIImage(named: "Placeholder")
    }
    
    private func formatTitle(_ title: String) -> String {
        // Maybe the title needs some formatting and that could
        // be done here…
        
        return title
    }
}
// The view controller now has a view model built from the model object
// and uses the view model to populate the view.
class ExampleViewController: UIViewController {
    let label = UILabel()
    let imageView = UIImageView()
    
    var modelObject: ModelObject
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Assume some code exists to set up the view before here.
        let viewModel = ModelObjectViewModel(from: self.modelObject)
        titleLabel.text = viewModel.title
        imageView.image = viewModel.image
    }
}

This does nothing to enable any kind of code reuse. At most it does allow for a separation of concerns between the model object and the format the view wants to present. The view controller will still be maintaining a tightly coupled definition of the data driving the UI, in this form you cannot reuse it for some other information, and the view controller has very intimate knowledege about the view and the view model types.

This won't do. The goal of a design system is to allow for reuse of UI elements that are created independently of the data in the application. Next I'll look at how I'd like to set this up for reuse.

Up Next

Enabling reuse with a protocol (coming soon)

Tagged with: