I started GUI programming with Visual Basic 6.0, then I learned how to use Microsoft MFC with C++, a while later I switched to Python and working with wxPython, which is a Python port for wxWidget. It has been more than ten years since I started working on GUI software. Recently I started working on iOS / OS X App projects in Swift, and interestingly I found that the essentials of building GUI apps are not changing too much, so are the problems I’ve been seeing so far. Despite we are still facing the same problems for developing GUI app, the good thing about software technology is that the solutions always improve over time, there is always new things to learn.
At very beginning, people builds GUI apps without any proper design. Overtime, new features added, bugs fixed, the system becomes an unmaintainable mess eventually. Then, design patterns was introduced, people started to write GUI apps with design patterns. In MFC era, there is a pattern called Document/View Architecture, it divides the application into two parts, Document is the one for business logic and data, and View is the part for presenting the Document. This pattern is actually a variant of MVC design pattern, but just the Document is actually the Model, and View and Controller were combined to be View. Since then MVC was widely known and used, the idea is to make Model the component in charge of manipulating data and business logic, Controller is for user events handling, and View is for presenting the Model.
With MVC architecture, ideally Model knows nothing about GUI implementations, which makes it
- Testable
- Portable
- Maintainable
Say if you develop a system for accounting with MVC architecture and MFC as the GUI library. One day you decided to make a port of your system to Mac OSX, as if the Model knows nothing about MFC implementation details, all you need to do is to rewrite views and controllers in Objective C for OSX (or you can use Swift as it’s a better option nowadays).
The problem with MVC
Although MVC solves some problems, there are still issues
Model changes bring huge impacts on View and Controller
As View and Controller have plenty conections to Model, as long as you modify the Model, you will need to modify both View and Controller when they happen to use the part of Model you modified.
View and Controller are not portable
As View and Controller need to be deeply integrated with the GUI library, which makes it hard to make it portable.
Responsibility of View and Controller is vague
One big problem of MVC is that the responsability of View and Controller is very vegue. View sounds like to be a component for presenting the Model only, but as in many GUI frameworks, UI elements usually also receives user interactions, so it can be handled in View, but usually Controller should take care of the user inputs. And for the Controller, it updates UI elements when Model data updates, so it’s actually presenting the Model? Since if Controller is updating UI elements, what does View do besides receiving user inputs? You can make the View transform data from Model and set it to the real UI elements, but Controller can also do that.
There are variants of MVC implementations out there, and interesting, they are similar but different in details. Some says Controller should update Model only, View should observe the Model, and some says Controller should update View, and also update Model, and View knows nothing about the model. I think this is a good evidence that the responsability of View and Controller is vegue, and given a real GUI framework, it’s very easy to mess View and Controller up altogether.
Hard to test View and Controller
A very common GUI programming problem to deal with is how to do automatic testing for the app you build. A traditional way to do it is to simulate user input events, such as mouse clicks, keyboard strokes. The problem of this approach is - it’s really really hard to test. In many cases, maybe due to UI animations or other weird UI features, your tests are likely to broken not because it’s not correctly written. Also, it’s very hard to mock with some UI relative objects since they are all implementation details. Moreover, when you write tests from UI interaction perspective, it also implies they are bound to a specific UI environment.
A better solution - MVVM (Model View View-Model)
To address the issues, MVVM was introduced. View-Model is a statful data layer for presenting the underlaying data Model, also provide operation for underlying data model, and View only translates and reflects state and data from View-Model.
As couplings were reduced down to
- View to View-Model
- View-Model to Model
View-Model should also know nothing about UI implementation details, which make it
- Testable
- Portable
- Absorbing change impacts from Model
The unsolved problem - how to deal with data binding
Unlike traditional web applications, GUI applications are very dynamic, say if you have a view presenting current temperature in real-time, then the temperature number could be updated in any time. A very primitive solution is to have a callback function property in the data model
class RealtimeTemperature {
var callback: ((temperature: CGFloat) -> ())?
func subscribe() {
// Subscribe to a server for the realtime temperature data,
// call `callback` function when we have a new value
}
}
Then you can write code like this to keep posted for the realtime temperature data
// say the self.model is RealtimeTemperature
self.model.callback = { [unowned self] temperature in
self.temperatureLabel.text = String(temperature)
}
self.model.subscribe()
But say
-
What if you need to update another UI element in another controller? Then you need to make the callback an array.
-
What if you want to cancel the subscription? Then you need to manually remove the added callback from the array.
-
What about error handling? What if we have network connectivity issue, how can we update the UI to let user knows it? Then you will need to add a second callback for error.
-
What about a new property to be updated? Well, then you need another callback array and error callback error for it.
Another serious problem with callback functions, it’s when ViewController destroyed, if you don’t unsubscribe the callback from the data model properly, the unowned self
hoding by the callback closure might still get used later, and will end up in crashing your app. To address that, you will need to cancel the subscription manually. Very soon, there will be tons of callback functions to take care, trust me, it will be a nightmare.
A better solution - Observer pattern
Since we only want to be notified when a certain event happens, and we don’t want the data model to know anything about GUI client, a better approach is to use Observer design pattern. For the real-time temperature example, and say we also added wind speed, the code could be modified like this
class RealtimeWeather {
let temperatureSubject = Subject<CGFloat>()
let windSpeedSubject = Subject<CGFloat>()
func subscribe() {
// Subscribe to a server for the realtime temperature data,
// notify temperatureSubject and windSpeedSubject observers when we have new values
}
}
Then, we can subscribe to the event as much as we want
// say the self.model is RealtimeTemperature
self.temperatureSubscription = self.model.temperatureSubject.addObserver { [unowned self] temperature in
self.temperatureLabel.text = String(temperature)
}
self.windSpeedSubscription = self.model.windSpeedSubject.addObserver { [unowned self] windSpeed in
self.windSpeedLabel.text = String(windSpeed)
}
self.model.subscribe()
and to cancel the subscription to the Subject
at anytime, all you need to do is to call the cancel
method of returned Subscription
object
self.temperatureSubscription.cancel()
And if you want to deal with error, you can add a Subject
for error like
let temperatureErrorSubject = Subject<NSError>()
But I bet you start feeling awkward already, error we are dealing with only has something to do with the temperatureSubject
, why not to combine them altogether?
Think about this
class RealtimeWeather {
let temperatureSubject = Subject<CGFloat, NSError>()
let windSpeedSubject = Subject<CGFloat, NSError>()
func subscribe() {
// Subscribe to a server for the realtime temperature data,
// notify temperatureSubject and windSpeedSubject observers when we have new values
// also notify them with error if we encounter one
}
}
Then you can also subscribe error from the same subject
self.temperatureErrorSubscription = self.model.temperatureSubject.addErrorObserver { [unowned self] error in
self.temperatureLabel.text = "Failed to gettemperature, error=\(error)"
}
// ...
Deferred (Promise or Future) for async operations
In fact, an observer pattern with both data and error callback is not a new idea. When I was working with Twisted (An async networking library in Python), there is a class called Deferred
, you can add the callback
and errback
to the object, and when the async operation finished, they will get called in a certain manner. For example
def callback(data):
print('page data' data)
def errorback(error):
print('error', error)
deferred = getWebPage('http://example.com')
deferred.addCallback(callback)
deferred.addCallback(errorback)
As all async operations returns Deferred
object, there is a standard way to deal with them, which makes it easy to provide common useful functions to manipulate them. For example, you can provide a retry function to retry an async function N times without modifying the code. Like this
d = retry(3, getWebPage, 'http://example.com')
d.addCallback(callback)
d.addCallback(errorback)
For the implementation details of this retry function, you can reference to my article: An auto-retry recipe for Twisted.
As this Deferred
approach for async operation is so good, then it was ported to JavaScript as PromiseJS
and many similar object in different programming language.
Although it eases async operation headache, it was designed for one time async operation rather than GUI, the needs are pretty different. For GUI, we want to keep monitoring changes of a subject instead of fire the request one time and get a result only.
Functional Reactive Programming with ReactiveCocoa
Given the problem and the solutions we had so far, you may ask, why not just combine these two paradigms altogether? Luckly, we don’t need to build this by ourselve, there are solutions available already, it’s called FRP (Functional Reactive Programming). It basically combines Observer pattern with Deferred and plus functional programming, it solves problems not just async operation, but also for GUI data updating and binding with view. There are different libraries for FRP in Swift, the most popular ones are ReactiveCocoa and RxSwift.
I like ReactiveCocoa more than RxSwift, as it has different types Signal
for emiting events and SignalProducer
for making Signal
(I will introduce them later), and according to Zen of Python
Explicit is better than implicit.
MVVM with ReactiveCocoa
For adopting MVVM, either View to View-Model or View-Model to Model should only knows the other party in forwarding direction, hence, to notify the bakward direction party, a good data binding mechanism is definitely inevitable, and it’s where ReactiveCocoa kicks in.
Although you can build your own data binding mechanisms like observer pattern or this SwiftBond described in this article, I don’t think it’s a good idea as you will probably end up with something pretty similar, which is in fact rebuilding a wheel.
Also, using Reactive approach not just solves the data binding issue, as modern App usually talks to server via API, we also need to deal with async operations. The Reactive solution comes with
- Stable solution that’s widely used and well tested for years
- Integrated solution for not just data-binding but also async operations
- Healthy communities providing third-party resources
- Build-in functions for manipuating event streams
Remember the retry
example we mentioned before, it’s also a build-in function for ReactiveCocoa, so you can retry any async operation you want without modifying a single line of code, just do it like
producer
.retry(3)
Besides that, say if you want to delay the result a little bit from a queue, not a problem, just call
producer
.delay(0.1, onScheduler: QueueScheduler.mainQueueScheduler)
And like what I said there are also other resources you can use, for example if you really like to use Alamofire with ReactiveCocoa 4, you can use the Alamofire integration for ReactiveCocoa I built - ReactiveAlamofire.
To be continued - a missing guide for MVVC with ReactiveX
From MVC came to MVVM with ReactiveX approach, this is the best solution I’ve ever learned so far. However, it’s not really widely used, I think that’s because Reactive code looks frightening at very first glance without spending some effort understanding why to use it and how it works. And there is also missing a practical guide shows you how things work. This is why I am writing this. The second part of this article will focus on how to use it.