Last active
March 26, 2017 10:56
-
-
Save mattgallagher/b93c6e587488d60a68e4cc0318a7c356 to your computer and use it in GitHub Desktop.
An abstract look at the View-Model/View-Binding pattern I frequently use in my projects and why this pattern uses the Swift `private` keyword.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This file is an abstract representation of code that I use extensively in my apps | |
// for constructing and maintaining views. The reason the `private` keyword is used here | |
// is to control the interface between two entities (a function and a class) which typically | |
// reside in the same file. | |
// | |
// The function and the class implement a View-Binding and a View-Model. The two are largely | |
// representations of the *same* concepts – the latter from data perspective and the former | |
// from a view-infrastructure perspective. Their inter-relatedness makes it highly desirable to | |
// place them both in the same file – they may share many small types between them and they | |
// are perpetually co-evolving. | |
// | |
// Another reason it is desireable to place them both in the same file is that it is common | |
// for the smaller implementations to be about 5-10 lines each. In the same way that over-long | |
// files are difficult to read, so is a project composed of too many trivial-length files. | |
// | |
// However, the View-Model is a large blob of state values – some of which are *safe* to bind | |
// to views are some of which are not. Herein lies the need for the `private` keyword. | |
// | |
// The existence of a `private` keyword allows enforcement of rules about safe/not-safe for | |
// entities in a single file. Without the `private` keyword, the programmer needs greater | |
// discipline and needs to have a clearer picture of how everything is supposed to fit together | |
// to avoid making mistakes. And accidentally violating an interface on a state machine isn't | |
// really the sort of problem that's easy to test. | |
// | |
// Without a `private` keyword, you must either: | |
// | |
// a) rely on comments to enforce interface safety rules (bad, bad, bad) | |
// | |
// b) promote the View-Model from `fileprivate` to `internal` and similarly promote every | |
// other type shared between View-Model and View-Binding to `internal` (this can be a large | |
// number of tiny little types when using command-pattern communication between the two) | |
// and move the View-Binding to a separate file. | |
// | |
// So (b) is less bad than (a) but is aesthetically inelegant and leads to large amounts of | |
// "bouncing" (flipping between the two in an editor) since, as I said, the two naturally | |
// "co-evolve" so changes to the ViewModel normally require refactoring the ViewBinding and | |
// vice-versa. | |
// | |
// Compared to (a), the `private` keyword enables safer code. | |
// Compared to (b), the `private` keyword enables more-aesthetic, simpler project structures | |
// that are easier to inline and/or faster to compile. | |
// | |
// Removing smaller-than-file access modifier granularity from Swift will necessarily hurt | |
// on one of these points, if not both. | |
fileprivate class MyViewModel: ViewModel { | |
// The view model maintains a connect to the dataModel. This is not private. | |
var dataModel: DataModel | |
init(dataModel: DataModel) { | |
(input, output) = SignalViaModel(dataModel: dataModel) | |
} | |
// The view can directly set this var | |
var viewSettableState = Something() | |
// This is a side-effect (maybe dependent state or a timer or file watcher or something) | |
// and should not be directly accessed by the view | |
private var internallyMaintainedState = SomethingElse() | |
// This variable can be safely accessed by the view for data binding purposes | |
var output: SignalMulti<Whatever> | |
// This variable is private (direct binding of an input to the view might result in | |
// premature closing of the signal) | |
private var input: SignalInput<Whatever> | |
// The view should use this function to send values to the `input` | |
func setInternallyPropagatedValue(some: Whatever) { | |
input.send(value: some) | |
} | |
} | |
// This function is the View-Binding. View-Binding is the construction of the view and the | |
// connecting of its actions and data observers to the view model. | |
public func constructMyView(dataModel: DataModel) -> MyView { | |
let viewModel = MyViewModel(dataModel: dataModel) | |
let myView = // constructed in code or from NIB file | |
myView.getTextField().bindTo(viewModel.viewSettableState) | |
myView.getButton().setAction { v in viewModel.setInternallyPropagatedValue(some: v) } | |
return myView | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment