Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Design Patterns in Swift: Document-View

DZone's Guide to

Design Patterns in Swift: Document-View

MVC is beautiful but sometimes you don't need a full-fledged controller.

· Mobile Zone
Free Resource

Discover how to focus on operators for Reactive Programming and how they are essential to react to data in your application.  Brought to you in partnership with Wakanda

Over the years, we've developed a few patterns to deal with building user interfaces to programs and dealing with dynamic data. We've had Model-View-Controller (MVC), Document-View (DV), Model-View-ViewModel (MVVM), and a few more (for an exhaustive list, see Derek Greer's excellent piece covering the history and use of every presentation pattern you've heard of, and a few you haven't). Though I'm not going to cover all of these, I would like to start discussing the most popular ones. It amazes me given how much we've discussed these patterns over the years that we still regularly get them wrong in custom implementations.

The Document-View Pattern. The document-view pattern was once the preferred pattern used in Visual C++ development. Microsoft built the original Microsoft Foundation Classes around this pattern, in fact, back in the dark ages. It’s not used much today, but it’s useful when you have simple data management needs. The problem with this particular pattern was the decentralization of data management logic, which tended to be spread around the document and view classes.

This pattern, originally, was built to address data view proliferation. Back then, engineers were building applications like spreadsheets, email clients, or word processors, and these were primarily rich clients running locally on some workstation. The more sophisticated applications had a client-server architecture with a data repository backend of some kind. Engineers were continually running into issues where they had to dynamically display the same data in multiple ways. Now, the MVC pattern would certainly have solved this problem too, but when the MFC designers adopted DV we didn’t have the kind of internet we have today. It’s very likely they weren’t aware of the pattern. They also may have felt it was too complex, and wanted a simpler data display architecture. Whichever the case may be, they used DV instead of MVC, getting rid of a separate controller class.

What is it? In the DV pattern, we have essentially two classes - a document class, which works as a model and an observer and can communicate with some backend repository, and a view class, which displays data and provides user data manipulation functionality. The document contains a reference to all the views, and each view contains a reference to the document, for a many-to-one relationship between views and documents. And it’s pretty easy to migrate from DV to MVC too, a definite plus. You can start with a simpler pattern and easily refactor to a more complex, powerful one if needed. More on this shortly.

Why use it? Well, it’s simple. It’s fast to build and easy to navigate.

Why not? It’s pretty disorganized. So the view object needs to not only render data, it also needs to handle data input. This can lead to bloated document and view classes, and a distribution of control logic across two separate classes, usually a bad idea. Realistically, this usually gets relegated to an aggregate object in more complex scenarios. And this, basically, is MVC. It also has circular dependencies, which are always fun to design around.

An example. This is a simple example, but it shows how data can update from some backend store through the document and how the views can also update the data based on control gestures from the user. First, let’s define some protocols:

public protocol Document {
    typealias EventType
    typealias KeyType
    typealias DataType
    var state : DataType? { get set }
    func addView(key: KeyType, eventHandler: (EventType) -> Void)
    func removeView(key: KeyType)
}

public protocol Event {
    init(message: String)
    var message : String { get }
}

Here, we have two protocols, one defining a document, and one defining an Event. The document references multiple view callback functions, while each view references a single document. Yay! Circular dependencies! Here, we get around this by manually adding each view to the document rather than supplying an initialization argument containing a list of views.

Next, using these protocols, let’s define some classes:

public class SimpleEvent : Event {
    public var message : String
    public required init(message: String) {
        self.message = message
    }
}

This is a simple event implementation. It packages a message into a simple event class. You’d frequently pass much more information than this in an event, perhaps even a pointer to a larger data object. In this case, we’re just passing a string to more clearly illustrate the pattern.

public final class StringDocument<T : Event> : Document {

    private var views : [String: (T) -> Void] = [:]
    private var data: String?
    public var state: String? {
        get {
            return data
        }
        set(state) {
            if let mystate = state {
                data = mystate
                if let mydata = data { notify(mydata) }
            }
        }
    }

    public func addView(key: String, eventHandler: (T) -> Void) {
        views[key] = eventHandler
    }

    public func removeView(key: String) {
        views.removeValueForKey(key)
    }

    public func removeAllViews() {
        views.removeAll()
    }

    private func notify(data: String) {
        let event = T(message: data)
        views.forEach { $1(event) }
    }

}


The Document implementation manages the document data and a collection of views that’re interested in updates to that data.


public final class StringView<T : Event,  
D: Document 
where D.EventType == T, D.DataType == String> {

    private let name : String = String(arc4random_uniform(10000))
    private var document : D
    private var data : String?

    init(document: D) {
        self.document = document
    }

    public func onDataChange(data: T) {
        self.data = data.message
        render()
    }

    public func submitData(data: String) {
        document.state = data
    }

    public func render() {
        if let mydata = data {
            print("(\(name)) data is: \(mydata)")
        } else {
            print("(\(name)) no data available")
        }
    }

}

Keep in mind, the View class is usually associated with some kind of visual user interface element. Here, for illustrative purposes, we haven’t implemented that kind of interactive control, though we have implemented the ability to change and render document data from a view, so the StringView class does behave like a view from the design pattern, though in a simple way. In the view class we can alter data, and when we do that, the document object is notified (and the document then notifies all the registered views). We also print the address of each view object in order to differentiate the objects when they’re printing submitted data. Really, very simple, but each class has the responsibilities outlined in the pattern. The document manages views and data from a backend repository of some kind. The view provides the ability to manage data, and presents the data to the user when it changes. Both the document and view classes reference one another. Here’s a simple example program using these classes:

typealias SimpleStringDocument = StringDocument<SimpleEvent>
typealias SimpleStringView = StringView<SimpleEvent, SimpleStringDocument>

var document = SimpleStringDocument()
var view0 = SimpleStringView(document: document)
var view1 = SimpleStringView(document: document)
var view2 = SimpleStringView(document: document)

document.addView(view0.name, eventHandler: view0.onDataChange)
document.addView(view1.name, eventHandler: view1.onDataChange)
document.addView(view2.name, eventHandler: view2.onDataChange)

[view0, view1, view2].forEach { $0.render() }

document.state = "blargh"

view0.submitData("foo")
view1.submitData("bar")

document.removeView(view2.name)

view0.submitData("blech")

Let’s run this example in a playground; what do we get?

(313) no data available
(4371) no data available
(460) no data available

After we create and add the new views, we render the document data from each view. The data’s yet to be assigned, so we have no data to view.

(460) data is: blargh
(313) data is: blargh
(4371) data is: blargh

We’re setting information in the document from some backend repository, and the document then notifies all the views of the update. The connected views then print the data that has been updated in the repository, carried in the event message.

(0x00007f956d900210) new data is: foo
(0x00007f956d81eec0) new data is: foo
(0x00007f956bd037a0) new data is: foo

Here, we’re viewing data to the user based on notification events received in the views from the document.

(0x00007f956d900210) new data is: bar
(0x00007f956d81eec0) new data is: bar
(0x00007f956bd037a0) new data is: bar

Again with the changing of the data via a view object. Then, for fun, we remove one of the views.

(0x00007f956d900210) new data is: blech
(0x00007f956d81eec0) new data is: blech

Here, we only notify two views of the change, so we update the data twice, once through each view.

That’s it! Very simple, as I promised. Document-view patterns are fast to build, easy to test, but only really work in simple cases.

Learn how divergent branches can appear in your repository and how to better understand why they are called “branches".  Brought to you in partnership with Wakanda

Topics:
patterns ,swift ,mobile

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}