The article is from collated notes on objccn. IO/related books. “Thank you objC.io and its writers for selflessly sharing their knowledge with the world.”
Complete five different implementations for the recording app using the following design patterns.
Two modes are widely used in iOS:
-
The standard Cocoamodel-View-Controller (MVC) is the design pattern Used by Apple in the sample project. It is the most common schema in Cocoa App, and it is the baseline from which schemas are discussed in Cocoa.
-
Model-view-viewmodel + Coordinator (MVVM-C) is a variant of MVC that has a separate “ViewModel” and a coordinator for managing View Controllers. MVVM uses data binding (typically used in conjunction with reactive programming) to establish a connection between the View-Model layer and the View layer.
The other three patterns to be discussed are not common in Cocoa and are still experimental architectures. These architectures provide useful insights into building your app, and can help improve your code even if you don’t choose to apply the entire architecture to your project:
- Model-view-controller +ViewState (MVC+VS) is a pattern that puts all View states in one place, rather than having them scattered across the View and View Controller. This is the same rule that the Model layer follows,
- ModelAdapter-ViewBinder (MAVB) is an experimental architecture used by one of the authors of the App architecture. MAVB focuses on building declarative views and ditched controllers in favor of binding to communicate between the Model and view.
- The Elm architecture (TEA) runs counter to common architectures like MVC or MVVM. It uses a virtual view hierarchy to build views and a Reducer to interact between the Model and view.
Model-View-Controller
In Cocoa MVC, a small number of Controller objects handle all tasks outside the scope of the Model or View layer.
This means that the Controller layer receives all the View actions, handles all the interaction logic, sends all the Model actions, receives all the Model notifications, prepares all the data for presentation, and finally applies it to the changes to the View. If you look at the diagram of the app feedback loop, you’ll see that on the arrow between model and View, almost every label is a Controller. The build and navigation tasks are not marked out and are handled by the Controller as well.
Here is a block diagram of the MVC pattern, which shows the main communication path of an MVC app:
The dotted lines in the figure represent runtime references. Neither the View layer nor the Model layer directly reference controllers in code. The solid lines represent compile-time references, and the Controller instance knows the interfaces of the View and Model objects to which it is connected.
1. Build
The App object is responsible for creating the top-level View Controller, which loads the view, knows what data to get from the Model, and then displays it. The Controller either explicitly creates and holds the Model layer, or obtains the Model through a deferred model singleton. In a multi-document configuration, the model layer is owned by a lower level like UIDocument or NSDocument. The individual Model objects associated with the View are usually referenced and cached by the Controller.
2. To change the Model
In MVC, controllers receive view events primarily through the target/ Action mechanism and a delegate (set by the storyboard or code). The Controller knows what view it is connected to, but the view has no information about the Controller interface at compile time. When a View event arrives, the Controller has the ability to change its own internal state, change the Model, or directly change the View hierarchy.
3. Change the View
In MVC, when a View action changes the Model, the Controller should not directly manipulate the View hierarchy. Instead, the Controller subscribes to model notifications and changes the View hierarchy when the notifications arrive. In this way, the data flow can be one-way: The View Action is converted into a Model change, and the Model sends a notification, which is eventually converted into a View change.
4. View State
View state can be stored in View or Controller properties as needed. In contrast to view actions that affect the Model, actions that only affect the view or Controller state do not need to be passed through the Model. Storage of View state can be implemented using a combination of storyboard, which records the active controller hierarchy, and UIStateRestoring. The UIStateRestoring is responsible for reading data from the Controller and view.
5. Test
In MVC, the View Controller is tied to the rest of the app. The lack of boundaries makes it difficult to write unit and interface tests for Controllers, and integration testing is one of the few remaining viable means of testing. In integration testing, build the view, Model, and Controller layers that are connected, and then manipulate the Model or View to see if you can get the desired results.
Integration testing is very complex to write, and it covers too much ground. It tests not only the logic, but also how the parts are connected (although in some cases not from the same Angle as the UI tests). However, with integration testing in MVC, it is often possible to achieve test coverage of around 80%.
The importance of MVC
Because Apple uses this pattern in all of its instance projects, and because Cocoa itself was designed for this pattern, Cocoa MVC is the official app architecture pattern for iOS, macOS, tvOS, and watchOS.
The freedom of MVC allows for many variations in use: many ideas derived from other patterns can be integrated into a small part of the entire app.
history
The name MVC was first proposed in 1979 by Trygve Reenskaug to describe the existing “Template pattern” application on Smalltalk-76. The MVC implementation in Cocoa dates back to the NeXTSTEP 4 years, circa 1997. Before that, all of the roles that controllers now play were usually played by a high-level view class (like NSWindow). Later, the idea developed from the original Smalltalk MVC implementation that the presentation part, the View layer and the Model layer, should be separated completely, created a strong need to introduce a support object to aid communication between the two.
The Model – View – ViewModel + coordinator
MVVM, like MVC, is architected based on scenes (subtrees in the view hierarchy that might be cut in or swapped out when navigation changes).
In contrast to MVC, MVVM uses view-Model in each scenario to describe the presentation logic and interaction logic in the scenario.
The view-model contains no reference to the View or controller at compile time. It exposes a set of properties that describe the values that each view should have when it is displayed. Applying a series of transformations to the underlying Model object yields values that can eventually be set directly to the View. The actual work of setting these values to the view is done by pre-built bindings that ensure that when the display values change, they are set to the corresponding view. Reactive programming is a great tool for expressing such declarative and transformational relationships, so it is naturally suitable (though not strictly necessary) for dealing with the View-Model. In many cases, the entire View-Model can be expressed declaratively in a responsive programming binding.
In theory, because the View-Model contains no references to the View layer, it is independent of the App framework, which allows testing of the View-Model to be independent of the App framework as well.
Since the View-Model is coupled to the scene, we also need an object that can provide logic when switching between scenes. In MVVM-C, this object is called a coordinator. The coordinator holds a reference to the Model layer and knows the structure of the View Controller tree so that it can provide the required Model objects for each scenario’s View-Model.
Unlike MVC, view Controllers in MVVM-C never refer directly to other View Controllers (and therefore, no other View-Models). The View controller tells the coordinator about the View action through the delegate mechanism. The coordinator displays new View Controllers and sets their Model data. In other words, the view Controller hierarchy is managed by the coordinator, not determined by the View Controller.
The overall structure of the architecture resulting from these features is shown below:
If you ignore the coordinator, the diagram looks a lot like MVC, but with a stage between the View Controller and the Model. MVVM transfers most of the previous work in The View Controller to the View-Model, but note that the View-Model does not have a reference to the View Controller at compile time.
The View-model can be separated from the View Controller and View, or tested separately. Similarly, the View Controller no longer has internal View states, which are moved to the View-Model. The dual role of the View Controller in MVC (both as part of the View hierarchy and responsible for coordinating the interaction between the View and model) has been reduced to a single role (the View Controller is just a part of the View hierarchy).
The addition of the coordinator mode further reduces the view Controller’s responsibility: now it doesn’t have to worry about presenting other View Controllers. Therefore, this actually reduces the coupling between View Controllers at the expense of adding a layer of Controller interface.
View-model has both View and Model in its name, but it acts like a Controller. A strict distinction will be made between a View Controller, which is just a subset of the latter, and a View Controller, which is just the part of the controller type that controls the view.
1. Build
For model creation and MVC persistence, it is usually the responsibility of a top controller. However, the individual Model objects now belong to the View-Model, not the View Controller.
The initial view hierarchy is created as in MVC, either through storyboards or code. Unlike MVC, the View Controller no longer gets and prepares data for each view directly; it hands that job off to the View-Model. The View Controller creates the View-Model when it is created, and binds each View to the corresponding properties exposed by the View-Model.
2. To change the Model
In MVVM, the View Controller receives view events in the same way as in MVC (and establishes a connection between view and View Controller in the same way). However, when a View event arrives, the View Controller does not change its internal state, view State, or model. Instead, it immediately calls methods on the View-Model, which in turn changes the internal state or model.
3. Change the View
Unlike MVC, the View Controller does not listen to the Model. The View-Model is responsible for observing the Model and converting the Model’s notifications into a form that the View Controller can understand. The View Controller subscribes to changes to the View-Model, which is usually done through a responsive programming framework, but can also use any other observation mechanism. When a View-Model event arrives, the View Controller changes the view hierarchy.
In order to achieve one-way data flow, the View-Model should always send the View action that changes the Model to the Model and notify the relevant observers only after the model change has actually occurred.
4. View State
View state exists either in the View itself or in the View-Model. Unlike MVC, there is no view state in a View Controller. Changes to View state in the view-Model are observed by the Controller, but the Controller cannot distinguish between notifications of model and notifications of View state changes. When using the coordinator, the View Controller hierarchy is managed by the coordinator.
5. Test
Because the View-Model and view layer are decoupled from the Controller layer, interface tests can be used to test the View-Model, rather than integration tests as in MVC. Interface tests are much simpler than integration tests because you don’t need to build a full component hierarchy for them.
For interface testing to cover as much ground as possible, the View Controller should be as simple as possible, but those parts that are not moved out of the View Controller still need to be tested separately. This section includes the interaction with the coordinator and the code that initially creates the work.
The importance of MVVM
MVVM is an indirect variant of the most popular MVC app design pattern on iOS. It’s not that different from MVC; Both are built around the View Controller scenario and use much the same mechanics.
Perhaps the biggest difference is the use of reactive programming in the View-Model, which describes a series of transformations and dependencies. Using reactive programming to clearly describe the relationship between model objects and displayed values provides an important guide to understanding dependencies in your application in general.
history
MVVM was proposed by Ken Cooper and Ted Peters, who were working at Microsoft at the time on what became Windows Presentation Foundation (WPF), the app framework for Microsoft.NET, It was officially released in 2005.
The coordinator in iOS has only recently (re) come into vogue, with Soroush Khanlou describing the idea on his website back in 2015. Coordinators are based on older models like App Controller, which have existed for decades in Cocoa and other platforms.
Model-View-Controller+ViewState
MVC+VS is an attempt to bring a one-way data flow approach to standard MVC. In standard Cocoa MVC, view state can be manipulated from two or three different paths, but MVC+VS tries to avoid this and make the handling of view State more manageable. In MVC+VS, all view states are explicitly defined and expressed in a new Model object, called the View State Model.
In MVC+VS, not a single navigation change, list selection, text box editing, switch change, model display or scroll position change (or any other view state change) is ignored. Send these changes to the View State Model. Each View Controller is responsible for listening to the View State Model, so communication of changes is straightforward. In the presentation or interaction logic, view states are not read from the view, but are retrieved from the View State Model:
The resulting diagram is similar to MVC, but part of the controller’s internal feedback loop (used to update view State) is different and now forms a separate View State loop similar to the Model loop.
1. Build
As with traditional MVC, applying document Model data to the view is still the responsibility of the View Controller, which also uses and subscribes to view State. Because both the View State Model and the document Model require observation, there are many more functions for observation via notification than typical MVC.
2. To change the Model
When a View action occurs, the View Controller changes the document model (which remains the same as MVC) or changes the Model state. Instead of directly changing the view hierarchy, all view changes are made via notifications from the document model and view State Model.
3. Change the View
The Controller observes both the document Model and the View State Model, and updates the View hierarchy only when changes occur.
4. View State
The View State is explicitly extracted from the View Controller. The controller looks at the View State Model and changes the view hierarchy accordingly.
5. Test
In MVC+VS, you use integration tests similar to MVC, but the tests themselves will be very different. All tests start with an empty root View Controller, and then by setting the document Model and view State Model, This root View Controller can build the entire View hierarchy and the View Controller hierarchy. The most difficult part of MVC integration testing (setting up all the components) can be done automatically in MVC+VS. To test another view state, you can reset the global view state, and all view Controllers will adjust themselves.
Once the View hierarchy is built, you can write two kinds of tests. The first test checks that the view hierarchy is set up as I expect it to be, and the second test checks that the View action changes the View State correctly.
The importance of MVC+VS
MVC+VS is primarily a tool for teaching view State.
In a nonstandard MVC app, add a View State Model and observe these view state models in each View Controller (based on the observations already made on the model), The benefits include arbitrary state recovery that does not rely on storyboard or UIStateRestoration, complete user interface logging, and the ability to jump between view states for debugging purposes.
history
This specific system is an instructional tool developed by Matt Gallagher in 2017, which is used to demonstrate concepts such as one-way data flow and time travel of user interfaces. The goal of this pattern is, with minimal changes to the traditional Cocoa MVC app, to make the state of the View snap-able with each action.
Model adapter-View Binder (MAVB)
MAVB is a binding – centric experimental model. In this pattern, there are three important concepts: View binders, Model adapters, and bindings.
A View binder is a wrapper class for a View (or View Controller) : it builds the View and exposes a list of bindings for it. Some bindings provide data to the view (for example, the text of a label), while others emit events from the view (for example, button clicks or navigation changes).
Although a View binder can have dynamic bindings, the View binder itself is immutable. This makes MAVB a declarative mode: declare view binders and their actions, rather than changing view binders over time.
The Model adapter is a mutable state encapsulation implemented by what is known as a Reducer. The Model adapter provides one input binding (for sending events) and one output binding (for receiving updates).
In MAVB, you don’t create a view directly; Instead, only the View binder is created. Likewise, mutable state outside of the Model adapter is never dealt with. The transformation (in both directions) between the View binder and the Model adapter is done by morphing the binding (using standard reactive programming techniques).
MAVB removes the need for the Controller layer. Create logic is expressed through the View binder, transform logic is expressed through bindings, and state changes are expressed through the Model adapter. The resulting block diagram is as follows:
1. Build
The Model adapter (to encapsulate the main Model) and the View State adapter (to encapsulate the top-level view state) are typically created in the main.swift file, which precedes any view.
The View binder is built using ordinary functions that take the necessary Model adapters as parameters. The actual Cocoa View is created by the framework.
2. To change the Model
When a view (or view Controller) can issue an action, the corresponding View binding allows you to specify an action binding. Here, data flows from the View to the output of the Action binding. Typically, the output is connected to a Model adapter, and the View event is deformed by binding into a message that the Model adapter can understand. This message is then consumed by the Reducer of the Model adapter and changes the state.
3. Change the View
When the state of the Model adapter changes, it generates notifications via output signals. In the View binder, you can deform the output signal from the Model adapter and bind it to a View property. This way, the View property automatically changes when a notification is sent.
4. View State
View State is considered part of the Model layer. View State Actions and View State notifications share the same path as Model Actions and Model notifications.
5. Test
In MAVB, you test your code by testing the View binder. Since the View binder is a list of bindings, you can verify that the bindings contain the expected entries and that they are configured correctly. You can use bindings to test the initial build and when changes occur.
Testing in MAVB is similar to testing in MVVM. However, in MVVM, the View Controller may contain logic, which can lead to untested code between the View-Model and the View. With no View Controller in MAVB, the binding code is the only code between the Model adapter and the View binder, making it much easier to guarantee complete test coverage.
Importance of MAVB
MAVB does not follow a direct precedent; it is neither a port from another platform nor a variation of another. It’s all its own, it’s experimental, and it’s a little weird, and it shows something very different. That’s not to say, however, that this pattern doesn’t draw lessons from other patterns: ideas like binding, reactive programming, domain-specific languages, and Reducer are already well known.
history
MAVB was first proposed by Matt Gallagher at Cocoa with Love. This pattern draws on Cocoa binding, functional response animation, ComponentKit, XAML, Redux, and thousands of rows of Cocoa View Controller experience. The example uses the CwlViews framework to handle view building, binder and adapter implementation.
Elm architecture (TEA)
TEA and MVC are fundamentally different. In TEA, the Model and all view States are integrated into a single state object, and all changes in the app occur by sending messages to the state object, which are handled by a state update function called reducer.
In TEA, each state change generates a new virtual View hierarchy, which consists of lightweight structures that describe what the view hierarchy should look like. The virtual View hierarchy lets you write the view part of the code as a pure function; The virtual View hierarchy always evaluates directly from the state without any side effects. Instead of changing the view hierarchy directly, we use the same function to recalculate the view hierarchy when the state changes.
The Driver type (which is part of the TEA framework that holds references to other layers in TEA) compares the virtual View tier to the UIView tier and makes the necessary changes to it to make the views conform to their virtual versions. The driver component in the TEA framework is initialized with the start of the app and does not know which particular app it corresponds to. This information is passed in its initialization methods: including the initial state of the app, a function that updates the state with a message, a function that renders the virtual View hierarchy based on the given state, and a function that calculates the notification subscription based on the given state (e.g., You can subscribe to notifications when a Model Store changes).
From a framework user’s point of view, TEA’s block diagram for the change section looks like this:
Tracing the top two layers of the diagram, you see a feedback loop between the View and model; This is a loop from view to state and back to view (coordinated through the TEA framework).
The following loop represents the way side effects are handled in TEA (such as writing data to disk): When a message is processed in the status update method, a command is returned, which is executed by the driver. The most important command is to change the contents of the store, which in turn is listened on by subscribers held by the driver. These subscribers can trigger messages to change the state, which eventually triggers a rerendering of the view in response.
The structure of these event loops makes TEA another example of a design pattern that adheres to the principle of one-way data flow.
1. Build
State is built at startup and passed to the run-time system (that is, the driver). At runtime the system owns state, and store is a singleton.
The initial view hierarchy is built along the same path as the updated view hierarchy: the virtual view hierarchy is calculated from the current state, and the runtime system is responsible for updating the real view hierarchy to match the virtual view hierarchy.
2. To change the Model
Virtual Views have messages associated with them that are sent when a View event occurs. The Driver can receive these messages and use update methods to change the state. The update method can return a command (side effect), such as a change you want to make in the Store. The Driver intercepts the command and executes it. TEA makes it impossible for a View to change state or store directly.
3. Change the View
The runtime system takes care of this. The only way to change a view is to change the state. So there is no difference between initializing the view hierarchy and updating the view hierarchy.
4. View State
View State is contained within the overall state. Since views are computed directly from the state, navigation and interaction states are also automatically updated.
5. Test
In most architectures, connecting test components to each other often takes a lot of effort. In TEA, you don’t need to test for this because the driver handles this automatically. Similarly, there is no need to test that the view changes correctly when the state changes. All you need to test is that the virtual view hierarchy can be evaluated correctly for a given state.
To test for a change in state, you can create a given state and then use the UPDATE method with the corresponding message to change the state. You can then verify that the UPDATE returns the expected results for a given state and message by comparing the before and after states. In TEA, you can also test whether the subscription for a given state is correct. Like the View hierarchy, the update and subscribe functions are pure functions.
Because all the parts (computing the virtual view hierarchy, update functions, and subscriptions) are pure functions, they can be tested in complete isolation. No initialization of any frame parts is required, just passing in the parameters and validating the results. Most of the tests in the TEA implementation are pretty straightforward.
The importance of Elm architecture
TEA was first implemented in Elm, a functional language. So TEA is an attempt to express GUI programming in a functional way. TEA is also the oldest one-way data flow architecture.
history
Elm is a functional programming language designed by Evan Czaplicki. Its original purpose was to build a front-end Web app. TEA is a pattern attributed to the Elm community that emerged as a natural result of the interaction between language constraints and the target environment. There is no definitive implementation of TEA in Swift, but we can find many research-based projects.
network
Using the MVC version of the app shows two different ways to add a network layer. In the first approach, the Controller owns the network, replacing the Model layer with a network service. In the second approach, the Model owns the network, and we add networking capabilities on top of the Model layer.
It would be easier to start by exploring the network owned by the controller: each View Controller performs the request as needed and caches the results locally. However, once the data needs to be shared between different View Controllers, this mode becomes difficult to operate. At this point, it’s usually easier to switch to the network model owns. By using the network as an extension of the Model layer, you can establish a mechanism for sharing data and change communication.
There are no patterns mentioned
Model-View-Presenter
Model-view-presenter (MVP) is a very popular mode on Android, and there is an implementation for it on iOS. It is roughly a pattern between standard MVC and MVVM in terms of overall structure and technology used.
The MVP uses a separate Presenter object, which plays the same role as the View-Model in MVVM. In contrast to the View-model, presenter removes the responsive programming part and instead exposes the values to be displayed as attributes on the interface. However, whenever these values need to change, the Presenter immediately pushes them to the view below (the view exposes itself as a protocol to the Presenter).
From an abstract point of view, MVPS are a lot like MVC. Cocoa’s MVC is, in all but name, an MVP – it was derived from Taligent’s original MVP implementation back in the 1990s. The View, state, and associated logic are the same in both modes. The difference is that modern MVPS have a separate Presenter entity that uses protocols to define the presenter and view Controller. Cocoa MVC allows controllers to refer directly to views, A Presenter in an MVP can only know the view’s protocol.
Some developers believe that separation of protocols is necessary for testing. When discussing testing, you’ll see that standard MVC’s can be fully tested without any separation. So MVPS aren’t that different. If there is a strong need to test a fully decoupled presentation layer, the MVVM approach is simpler: Let the View Controller pull values from the View-Model by observing, rather than having the Presenter push values into a protocol.
VIPER, Riblets, and other “Clean” architectures
VIPER, Riblets, and others like it tried to bring Robert Martin’s “Clean Architecture” from Web apps to iOS development by basically dividing the responsibilities of controllers into three or four different classes, Arrange them in a strict order. Each class in a sequence is not allowed to refer directly to the previous class in the sequence.
In order to enforce the rule of single-direction references, these patterns require a very large number of protocols, classes, and ways of passing data across different layers. For this reason, many developers who use these patterns use code generators. These code generators, and any pattern that is so complex that it requires a generator, are misleading. Attempts to bring the “Clean” architecture to Cocoa have often claimed that they can manage the “fat up” problem of View Controllers, often making the code base bigger.
While splitting interfaces is an effective way to control the size of your code, this should be done on demand, not dogmatically for each View Controller. Shredding interfaces requires a clear understanding of the data and the tasks involved in order to achieve optimal abstractions and minimize code complexity.
Component-based Architecture (React Native)
If you choose to use JavaScript instead of Swift programming, or if your app relies heavily on Web API interaction, JavaScript is a better choice, and React Native may be considered.
If you’re looking for something like React Native, but based on Swift, check out the Search for TEA. MAVB’s implementation also takes some cues from ComponentKit, which itself takes cues from React: it uses the syntax of class DSLS for declarative and deformable view construction, This is very similar to the Render method of the Component in React.