Swift app architecture (without the “Architecture”)
There seems to be a new article on app architecture every other day at the moment; MVC vs MVVM vs VIPER and so on. This article is not about the benefits or drawbacks of any given (big A) “Architecture”; its about (small a) architecture; the little decisions that make any architecture at least a little cleaner.
Lets start with a simple one:
struct DataModel
{
var name: String
}class MyView: UIView
{
let label = UILabel()
let button = UIButton() func configure(with data: DataModel)
{
label.text = data.name
}
}class MyViewController: UIViewController
{
let myView = MyView()
let myData = DataModel(name: "Bob") override func viewDidLoad()
{
super.viewDidLoad() myView.configure(with: myData)
myView.button.addTarget(self,
action: #selector(MyView.didTap(_:)),
for: .touchUpInside)
} func didTap(sender: UIButton) {
// do something
}
}
This is a very typical pattern that we see in iOS apps. There are a couple of issues with it to my mind though;
- There are very strong dependencies between the view and the view controller and the model — the view knows about the model, and the view controller reaches into the view, to get the button to apply the tap action.
- There is very little we can reuse.
Lets propose something a little different.
First up, there are a bunch of tutorials out there on how to replace the target/action call with a closure; Jackie Wang has a nice clear one — we’ll use that example, and for the purposes of the rest of this article assume it has been added to our project.
Now we have this:-
struct DataModel
{
var name: String
}class MyView: UIView
{
let label = UILabel()
let button = UIButton() func configure(with data: DataModel)
{
label.text = data.name
}
}class MyViewController: UIViewController
{
let myView = MyView()
let myData = DataModel(name: "Bob") override func viewDidLoad()
{
super.viewDidLoad() myView.configure(with: myData)
myView.button.addTargetClosure { _ in
// do something
}
}
}
Better; although it seems like a trivial change, it means that the configuration of the view is fully contained in the same function in the view controller and more importantly, it makes the configuration of that action portable ie: it is no longer tied to the view controller.
For example, we can pass the action closure into the view:-
struct DataModel
{
var name: String
}class MyView: UIView
{
let label = UILabel()
let button = UIButton() func configure(with data: DataModel,
action: UIButtonTargetClosure)
{
label.text = data.name
button.addTargetClosure(closure: action)
}
}class MyViewController: UIViewController
{
let myView = MyView()
let myData = DataModel(name: "Bob") override func viewDidLoad()
{
super.viewDidLoad() myView.configure(with: myData, action: { _ in
// do something
})
}
}
Now the view controller now knows nothing about the view except that it needs to be configured with an instance of DataModel and an action of type UIButtonTargetClosure. However, even though the view controller is much clearer, there is still a hard dependency between the view and the model, and having the action in the view seems backward …
Lets take a step back and think about what we’re trying to achieve here …
- We want to configure the view with a model
- We want to add interaction between the view and some action — a delegate call or service call perhaps.
How about we extract those parts?
struct DataModel
{
var name: String
}class MyView: UIView
{
let label = UILabel()
let button = UIButton()
}class MyViewController: UIViewController
{
let myView = MyView()
let myData = DataModel(name: "Bob") override func viewDidLoad()
{
super.viewDidLoad() DataConfigurator.configure(view: myView, data: myData)
ViewInteractor.configure(view: myView, action: { _ in
// do something
})
}
}struct DataConfigurator
{
static func configure(view: MyView, data: DataModel)
{
view.label.text = data.name
}
}struct ViewInteractor
{
static func configure(view: MyView, action: UIButtonTargetClosure)
{
view.button.addTargetClosure(closure: action)
}
}
It might seem quite a convoluted way around it, but what we’ve got now is:
- A function for configuring an instance of MyView with an instance of DataModel; we can test this independently of the rest of the application, and we can reuse it for any instances of those.
- A function for defining the action(s) of an instance of MyView; again this can be tested independently of the rest of the application, and we can reuse it for any instance of MyView and any action.
- The controller, which always did know that it had an instance of MyView and DataModel still calls the configuration, but now the view knows nothing of the model, and is just concerned with being a view!
- An extensible and cleanly separated way of defining any number of configurations and interactions.
We could go further and make the view and model arguments in the configurator and interactor into protocols or generics, so that we’re not even depending on a specific type of view or model, but that really depends on the requirements of the project — it’s easy to get into generalisation at the expense of clarity.
In my own projects I have been using simple static functions (usually related functions grouped together under a “configurator” or “interactor” struct) to configure view-model connections or view-actions. My UIView subclasses are generally very simple now, with a few properties (combined with “ViewStyle” functions) and not much else, and my ViewControllers are similar.
The nice thing about this approach is that it’s applicable to any Architecture style — MVC, VIPER, MVVM etc; all we’re doing is extracting set-up into standalone functions.
I primarily use ReactiveKit + Bond, and this approach lends itself really well to a reactive architecture — the “configurator” and “interactor” functions set up the all the bindings between different elements of the application, whether those are views, user interactions, models or system events. Combined with factory methods or a dependency injection library, the view controller shrinks even more, because now the factory or DI lib calls the configuration.
John Sundell had a good post last week about this topic (Preventing views from being model aware in Swift), which approaches the subject in a similar way, though he’s creating new configurator objects, rather than simple functions to do the work. He shows in particular using the configurator for UITableView (or UICollectionView) cells which the pure function version works well for too.
Although I’ve found the function approach to be very successful, the object version has it’s own advantages; the implementation is not the real takeaway here, it’s the stopping and asking “what are we trying to achieve” and then using the right programming tool for the job. For example, it’s all too easy when you’re a OOP programmer to look at everything as objects, or to see the power of protocols and protocol extensions in Swift and forget about what you can achieve with just functions.
✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.