Try swift nyc matt gallagher header

Driving View-State through Data for Fun and/or Debugging

A look at modeling view state as a separate, serializable model within your app and the functionality, fun and/or serious debugging you can unlock by driving your view state through data rather than presentation, including jumping forwards and backwards in time, replaying your user-interface and being able to log and inspect your view state to ensure program correctness.


Introduction

I’m going to be talking to you today about view-state driven applications. I write a blog called Cocoa with Love – it’s great if you haven’t seen it. You can find me on Twitter at @cocoawithlove.

My goal today is to have everyone think about view-state in your applications. It’s a bit confusing because we don’t pay a lot of attention to view-state. View-state is something we might know about, but in standard Cocoa application design pattern it’s largely ignored or forgotten.

What I’m going to look at is what happens if we take this mostly forgotten aspect and literally make it drive the whole application?

Ideal Model-View-Controller Change Pattern

Rather than start by talking about view-state, I’m going to start by talking about the standard Cocoa Application Design Pattern. You’ve probably seen diagrams like this one here that Apple used to demonstrate Model-View-Controller, but that’s not incredibly helpful for this.

I’m instead going to focus on a slightly different pattern, which is how a change flows through an application that follows this pattern: The Model-View-Controller Change Pattern. The idea is that we have our view, and when our view does something, it talks to the controller.

driving-view-state-slide-5

When you click on a button or you edit text in a text field, that action gets sent to the controller, and the controller chooses to change the model. Then the model is observed from the outside by an observer (which is probably just another controller) and the controller then chooses to update other views.

An important thing to note about this pattern are the two dashed lines in the diagram. These are runtime bindings – situations where you haven’t necessarily written in code how these things are connected. Certainly there isn’t code at both ends of the connection, and you can add additional observations of what’s going on at runtime.

Another thing to note about this is the concept of a model context. The idea that what we try to do with our models is have them have a clean interface on the front–a narrow interface. Even though you might have lots of things that try to change the model, they all have to go in through one point and then when the model changes, it has to come out through another point; your observations.

Reasons for Data-Driven Patterns

There’s a clean and narrow interface into and out of the model, and things are not directly coupled. There’s runtime bindings so you can add additional observers easily to something. If another controller wants to pay attention to a piece of state you’ve changed, it can just observe the same thing and you don’t need multiple controllers to actually coordinate. They coordinate by design rather than hard-coding everything.

Get more development news like this

That’s really the reason why we focus on model-view controller in apps. It’s to get this clean change pattern through our whole app. It makes testing easier, because it’s easy to test the model if we can separate it out from the views. Views can coordinate. If you want to just add another observer to the same piece of state, then you can add another observer to the same piece of state. Multiple views in your app can update at the same time when the model changes.

It reduces the need for knowledge. We don’t have to hard code the fact that this view over here wants to know about that model state when it changes, so whoever changes the model doesn’t need to tell it directly. It’ll just find out when the model changes. Because everything happens on the same path, we can do interesting things. We’ve got this narrow interface and we can play with it.

Data-Driven vs. Presentation-Driven Patterns

I’m going to show you an app that I’ve written that’s based on the standard master-detail template that Apple has in Xcode. It’s got master controllers and detail controllers, as you’d expect.

It shows times in different time zones around the world, like the Clock app on the iPhone, but you can scroll up and down in this view. You can select one of the things. You can even change the name of the time zone if you want to personalalize it. I renamed one “try! Swift”. I can also add additional time zones if I want. I can add Cairo to the list and Cairo, or I can remove Brisbane.

I bring this app up just to show one of the advantages you can have from having a standard change pattern going through the document. The benefit of going through that narrow interface and coming back out the other side is that we can hook in things like undo and redo. This slider at the bottom of the screen can undo the changes I’ve made.

But the other thing you can see in this is that there’s a kind of inconsistency. When I renamed New York, I wasn’t on the master view of all the time zones, I was in fact in the New York detail view.

There’s more that is mutable about the app than just the document. There’s also the state of where I was in the app, the scroll position, whether or not I have the selection for time zones showing or not, etc. These are not committed to the document, and yet they are mutable in the app. Simply winding the document back doesn’t affect these other things – when I wind the slider back, I’m still stuck permanently on this master view.

We have two separate sets of rules. We have document data that goes along this data-driven pipeline and we have view-state.

The view-state actually goes along what’s called a presentation pipeline. The idea is that instead of changing data and then responding to changes in that data, instead we bring up a presentation, like a view controller, we perform a segue, and then we set the data after the fact.

trySwiftNYC2017 9

Instead of the first pattern that I showed you, we have this shortened pattern which just goes view to controller, segues to another controller, and brings up a view. It may or may not actually bring up any data, and many times it doesn’t. For example when I bring up the selection controller it doesn’t actually contact the model for any data.

We have these truncated patterns and it’s truncated a little bit more than you can even easily see in the diagram, because the top one has the model context so it has this consistent interface that’s going into and coming out of. The one on the bottom doesn’t – there’s no consistency about it. Every controller is separate, hard-coded, and running on its own different sets of rules.

Demonstrating the Difference

I want to show you in code the difference between these two ideas. The data-driven path is at the top here. If we delete a row in a table, that’s a data-driven path.

matt-gallagher-driving-view-state-through-data

The first thing we do is to remove the object with: Document.shared.removeObject(sortedObjects[indexPath.row]) Then at a later point, we handle a change notification from the store. We sort the objects and reload the data in the table. That’s a data-driven change.

A presentation-driven change occurs when we select a different item for the detail views. We click on a row in the master view and it brings up the detail view.

Unlike the previous example, the first thing we do is not change the data, but change the presentation with performSegue. Then at a later point in the prepare(for segue:), we finally set the data on the controller.

You can see the line controller.timezone equals timezone. It finally sets the actual data for the view. In a presentation-driven path, we have performSegue first, then only after are we changing the data.

I want to point out a line to you, the ugliest line there on the slide, which is:

let controller equal (segue.destination as! UINavigationController).topViewController as! DetailViewController

This comes straight out of the template. The idea is that if you need to change the detail view in a master view controller, the master view controller, despite its name, isn’t actual at the top of the view hierarchy. It’s actually a junior view of the split view. But it needs to know about the whole hierarchy. It needs to know the detail view has a navigation controller in it, and it needs to know the top one is going to be a detailed view.

Because we don’t have anything that’s actually coordinating all of the patterns across the app, because we’re not observing a common point, when we have a presentation-driven approach, we have to hard-code knowledge about the whole app into everywhere where a change occurs.

What are the Consequences?

The consequences of this presentation-driven approach that affects our view-state across the app is that we have a coupling between cause and effect. The master view controller needs to know everything about the detail controller. It becomes difficult to add dependencies because the detailed controller wants to know what’s selected in the master view. Every time anything else wants to know what’s selected in the master view, it’s also going to need to have all the extra data added to it. Controllers are intercommunicating in order to send segue data and it’s just a bit crazy. Plus, there’s no clean interface or other testing hooks because the data is hard-coded. No data is going through a common place.

We have this situation where we have a data-driven approach for our model data, for our documented model, but we’re failing to apply the same lessons that we’ve learned over the years to our view-state. In a small app like this, there’s in fact more view-state than there is actual documented model. We want our view-state to be tested and persisted in a well-behaved iOS app. We should be using UI state restoration and other things in order to preserve all of them. But it’s difficult if we’re not paying attention.

Everything Mutable in your App is Actually a Model

Whether or not you’re representing it as such, every thing mutable in your app is actually a model. View-state is a model. Changes to view-state should ideally be be taking a data-driven path. We want all aspects of our app to share in the advantages of the data-driven path.

class Document: NotifyingStore {
	let url: URL
	private (set) var timezones: [UUID: Timezone] = [:]
}

struct Timezone: Codable {
	var name: String
	let identifier: String
	let uuid: UUID
}

Let’s start by looking at the actual document, the normal model document of the app. It’s an app that just presents timezones. Most of the documented model is just that dictionary of timezones. The dictionary is the UUID, which is nothing but a key for the row, and Timezone structs. The Timezone structs have a name that you can change, an identifier (which is the timezone), and the UUID, which is nothing other than a row ID.

The important consideration to ask yourself is: what is mutable in the app? We have the mutable timeszones, and we have the mutable name.

Let’s think about what’s actually mutable in the views of this app.

  • The views have a scroll position on the master view.
  • Whether or not I’ve selected a detail item.
  • Whether or not we’re in editing mode.
  • Whether or not the select timezone screen has come up.
  • Scroll position on the detail view.
  • The search text field.

Notice I didn’t mention the text field in the detail view because that’s in the documented model. The search text field doesn’t go via the document, so it’s truly view-state.

What we want to do is make all of these mutable things into their own model. These are mutable aspects of the app’s view-state. It has this split view-state, which always has a master view. Plus, it might have a detail view, and it might have a selection view.

The master view has its scroll position and whether or not it’s in edit mode. The detail view actually doesn’t have any mutable elements. The only interesting part of it is whether or not it exists at all. And then it just has the UUID as an identifier. And then the selection view, it has a scroll position, and it has that search text field at the top.

struct SplitViewState: Codable {
  var masterView: MasterViewState
  var detailView: DetailViewState?
  var selectionView: SelectionViewState?
}
struct MasterViewState: Codable {
  var masterScrollOffsetY: Double = 0
  var isEditing: Bool = false
}

struct DetailViewState: Codable {
  let uuid: UUID
}

struct SelectionViewState: Codable {
  var selectionScrollOffsetY: Double = 0
  var searchText: String = ""
}

Another advantage of having models is that they are abstract representations of the app. The actual model that I showed you, the document, just had the timezones and a name and a UUID and an identifier. It was about 10 lines. Here I have another 30 lines. And between the two of them, they represent the whole 600 line app.

These represent all of the view-states of the app. These are the things that we’re really thinking about when we’re writing the app. Having this kind of model is a great way of thinking about your app. When you ignore all of these things you might not have a good idea in your mind of what you’re really doing.

A Representation of View-State

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 performSegue(withIdentifier: "showDetail", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if segue.identifier == "showDetail" {
    if let indexPath = tableView.indexPathForSelectedRow {
    let timezone = sortedTimezones[indexPath.row]
    }
  }
  let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
  controller.timezone = timezone
}

How do we apply this kind of model? Previously, when we wanted to bring up the detail view, we’d use the code above, which I’ve shown you before. It does the performSegue, then it sets the controller’s timezone immediately after digging into the DetailViewController.

To do it in a data-driven approach, instead of performing segue as the first action, we set the detail selection. It’s the first green line under the view-state driven, data-driven heading there.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 ViewState.shared.setDetailSelection(uuid: sortedTimezones[indexPath.row].uuid)
}

@objc func handleViewStateNotification(_ notification: Notification) {
  let detailView = ViewState.shared.topLevel.detailView
  if let uuid = detailView?.uuid, Document.shared.timezones[uuid] != nil, lastPresentedUuid != uuid {
    lastPresentedUuid = uuid
    performSegue(withIdentifier:
      notification.userActionData != nil ? "detail" : "detailWithoutAnimation", sender: self)
  }
}

The first interesting line is ViewState.shared.setDetailSelection. It sets the detail selection on the view-state model. Then all we do is in the same way that we prepare(for segue:), instead we now handle that view-state change and the last thing we do is performSegue. We’re now changing the data first and then the whole view-state of the application responds to that change.

We’ve gone from presentation and then data, to data and then presentation. We’ve literally turned the whole app upside down, and the insignificant part, the view-state, is now the most significant part, and the presentation is now less significant. It’s just a response to the view-state changing.

Time traveling through your app

Because view-state is a model and the model is a model (which I call the document model to distinguish it), we can wind them back at the same time. Instead of just undo and redo on the model we now go all the way through the entire state of the app.

But there’s another point, which is that they are models, the view-state model and the document model, but they’re not the same model. We don’t have to wind them back at the same time. We have this ability to control view-state and model independently, so we can test scenarios that might have been difficult to get into otherwise.

The ability to wind back view-state is sometimes called time travel in different design patterns. It’s very common to see time travel happening in uni-directional data flow, which is a way of putting your entire app on this tight little loop. Used by libraries like ReSwift.

You don’t have to completely redesign the architecture of your whole app to do things like this. A view-state approach is really just thinking about your view-state as a model and putting it on a model pipeline. The clock sample app that I showed you is very close to Apple’s master detail app template. All I’ve done is add a view-state model that isn’t in the standard template and made sure that the same changes that I’m doing, the same path that I’m taking for the model data is the same path taken when I update view-state.

Testability

Time travel through our app’s state gives us the ability to test things. On its own, time travel isn’t useful for users. But testing of user interfaces is notoriously difficult because it’s very hard to decouple the entire Cocoa framework from the aspect that you’re trying to test.

Meanwhile, models are very easy to test. They’re not connected to the whole app. We can observe the model and make sure that as we’re making changes the correct notifications are sent out and the correct state changes occur.

I don’t even necessarily mean unit testing. I mean, what if you’re just playing with your app and something quirky happens. If you’re logging the view-state as you go along then you’ve got little sections in your log like below:

{
  "selectionView": {
    "selectionScrollOffsetY": 1669,
    "searchText" : "Melbourne" 
   }, 
  "masterView": {
    "masterScrollOffsetY": 668,
    "isEditing": false
   }
}

You can tell exactly which state the app was in when it did that weird behavior. If you want to fix that problem then, all you need to do is copy that JSON out of your log and play it back in the app later, and you can make your app go from states that might be difficult to transition between ordinarily.

Disadvantages

There are some drawbacks to this approach. The biggest one is the shortened presentation-driven pipeline that I drew went view, controller, controller, view. It was this tiny little thing. That actually does mean it’s less code. But it’s code that you write every single time and so it’s difficult to test code.

Putting things through the model, having a whole model interface set up and making sure you’re handling notifications coming out of the model feels like a little bit more code to start with, but it’s code you’re already doing for your model data. It’s not something you’re unfamiliar with, it’s just a little bit more of the same and you get all of the benefits discussed earlier.

I want you to think about view-state. Pay attention to all of these little bits of state everywhere in your app, that you might not be capturing, that you might not be paying attention to. Because in reality, everything mutable in your app is part of a model. Whether of not you’re representing it as a model, it is a model.

The project is available on my GitHub, and you can find more on my website.

Next Up: Realm for iOS #2: Tutorial: Building a To-Do App with Realm

General link arrow white

About the content

This talk was delivered live in September 2017 at try! Swift NYC. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Matt Gallagher

Matt Gallagher started his career writing embedded C at a printer company and computer vision research at a video games company. For the last decade, Matt has worked as a Mac and iOS developer and consultant across a range of fields from video server software to weather apps. His website, cocoawithlove.com, has offered in-depth articles on Mac and iOS development since 2008.

4 design patterns for a RESTless mobile integration »

close