SwiftUI: State-Model-View architecture

In the documentation for UIKit Apple can find an explanationthat “the application structure is based on the Model-View-Controller design pattern (MVC)”.

In Apple's materials SwiftUI There seem to be no explanations or even just references to design patterns. Let's try to figure out why first. Next, we will consider logical and simple solutions for building both individual components and the application level using states and property wrappers; an approach that can be logically described as State-Model-View.

Why the topic of patterns is difficult

The topic of using templates for application architecture is considered complex. Beginning developers struggle with it a lot and for a long time. It is more complicated not because they themselves are incomprehensible ideas design patterns with acronyms that often arose long before mobile applications, and the fact that “inventors” come up with some “new” abstract entities along with sophisticated methods of how to connect them with each other. TA-dah, Coupling here sends us a big hello! 🙂

Imagine a respected junior who cannot grasp practical expediency application of certain patterns only because this is what is usually not discussed with code examples that allow you to understand why it is better to live with a pattern than without it. The matter is limited to abstract theses that the reader needs to take on faith, for example, that this [подставьте свой любимый] the template “solves the assembly problem”, “facilitates the process of developing complex applications”, “allows you to separate the logical parts of the project into different objects” and even horror stories that “a simplified approach to architecture will certainly play a cruel joke”.

Let's not argue with respected authors and take these theses on faith. Let's look at what Apple itself says about the architecture for SwiftUI applications. Hmm, looks like almost nothing.

However, it’s easy to find a humorous presentation on the Swift developers forum Stop using MVVM for SwiftUI. It says that there is no need to engage in over-engineering, but it is better to use simple solutions in the SwiftUI logic instead of artificially complicated ones. All this with comparisons of code fragments. In passing, we note that the architecture there is jokingly named as MV without C, that is Model-View without Controller. Extensive comments on the post with different points of view and examples are also very valuable.

People are not interested in discussing simple and understandable things; the topic of using patterns to design code is really complex.

Still, it seems that any, even a very simple application on SwiftUI is not very successfully described as MV, since it consists not only of a data model and views for their visualization. Lacks glue, which connects them. Let's return to this topic a little later.

About declarative syntax

I suppose that engineers Apple is well aware that new principles should not be multiplied without special needso in the documentation can be found this phrase:

SwiftUI uses a declarative syntax, so you can simply state (state underlined by me) what your user interface should do.

What does this mean? declarative syntax?

Let's look at a simple example.

Snippets of this example and most of the subsequent ones for SwiftUI are taken from here

Here the developer has placed two visible elements and one invisible instance in a horizontal container Spacer, which pushes other components to the left. If this Spacer post above post.image, then the elements will be placed on the right edge. If you flip the screen of a simulator or physical device from portrait to landscape, then all components will move correctly. If you enable dark mode in iOS, the background and text colors will automatically switch. On different devices, including iPad, the visual will be scaled differently, taking into account a different resolution and aspect ratio of the screen.

That is, the developer is here declared the principle of layout of this screen with the necessary components, set where restrictions are necessary, and SwiftUI automatically built a visual interface. And we must admit that SwiftUI in the vast majority of cases does everything cleanly and correctly imperative screen rendering work.

It’s great if SwiftUI is all declarative, then…

Is UIKit an imperative framework?

Eh, marketers are not engineers. They know their job, so their narratives easily take root in the minds of developers. Can UIKit methods be called “imperative” when programmers use, for example, the following descriptions:

private func setupConstraints() {
  NSLayoutConstraint.activate([
    aView.topAnchor.constraint(equalTo: self.view.topAnchor),
    aView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
    aView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
    aView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)          
    // etc.
  ])
}

Here, in essence, a description of the limitations of a system of linear equations is specified for solving them inside the engine without the participation of a programmer. This example is the very quintessence of declarativeness.

However, if see primary sources from WWDC 2019, where a lot was said about declarativeness, then the dilemma “imperative UIKit vs declarative SwiftUI” was rather proposed by various subsequent commentators. These are the patterns of human thinking – to look everywhere and find dichotomies that marketers use for their own purposes.

And then what development concept does UIKit implement? For a 90-year-old developer who has recently mastered OOP, the answer would be quite obvious – this event driven architecture (event-driven), closely related to the object-oriented approach. Yes, indeed, here it is quite old but relevant description from Apple.

Interestingly, although using the inheritance hierarchy in OOP and creating your own custom classes is not difficult in principle, it seems that developers often “simplify” their lives with clever tricks, such as creating instances of classes from wrappers with closures:

var aButton: UIButton = {
  let button = UIButton()
  button.translatesAutoresizingMaskIntoConstraints = false
  button.setTitle("Login", for: .normal)
  button.setTitleColor(.white, for: .normal)
  button.addTarget(nil, action: #selector(touchTheButton), for: .touchUpInside)
  button.layer.cornerRadius = 12
  button.clipsToBounds = true
  return button
}()

instead of the object-oriented counterpart with a separate class:

let aButton = MyButton(title: "Login",
                       titleColor: .white,
                       action: touchTheButton)

Isn’t this where, among other things, the “massive view controllers” problem is more fictitious than real? Eat simple solution methods this problem.

But let's return to SwiftUI.

State-Model-View Architecture

The concept of a logical architecture for SwiftUI is obvious. For visualizing the interface through views, variables are of particular importance; they have the meaning of states and, in run time mode, have the function of triggers.

Here is a typical example where depending on the state of a boolean variable wrapped in a property wrapper @State one is displayed Viewrepresenting here the actual question of a quiz on the SwiftUI framework or a completely different one, showing the final result of the quiz:

For views with more than two, you can use the construction switch. A code example is at the end of the article.

Question: How does the binary variable change its state in the example above? Answer: through binding and not in native QuizViewand in the nested QuizQuestionViewwhere it is passed through the structure initializer with the prefix $

Here is the snippet with QuizQuestionView:

Let us pay attention here to a fundamental feature of the method of transmitting data through binding in SwiftUI: we do not notify the root view in any way about a change in the state of a variable quizzing, indicating whether we are in a quiz state and do not take any action to hide the screen when the test is already completed. We simply change the state variable at the right time. Next, the engine automatically transfers the state change to the root view and thus changing the trigger state completely changes the appearance of the screen from the quiz to its result without any other imperative regulations and conditions.

A similar concept is used to display modal screens sheets and alerts:

Here the code contains two variables, the state of which determines whether the alert is displayed on the screen, and after the answer “Yes” a new instance is added to the display hierarchy SheetView.

Modifiers .alert And .sheet it is not necessary to adapt to the button, they can be placed on any views in the local scope of the property body.

That is, the architecture of the user dialogue – the screen rotates approximately according to this pattern:

It is quite obvious that the key element here is the engine under the hood of the mechanism for binding state variables to wrappers that have state in their name (@State @StateObject)

Essentially this architecture revolves around State, date Model (using SwiftData in these examples) and composition Views.

By the way, about the Views composition:

Lego constructor for the Views hierarchy

Everyone loves Lego constructors, and constructing according to this principle in SwiftUI is easy and enjoyable if you master passing model data through initializer parameters in one of the ways, as was demonstrated in the examples above.

In SwiftUI, it’s easy to make a small component View from two to three elements (as in the first snippet), then place it in one or more more complex nodes, and so on, until the application as a whole is assembled from elementary bricks in the skillful hands of a designer. Of course, not everything is so simple, but still the principle better use this one.

Here's an example of one screen with three different Swift Charts:

Each diagram is made as a separate component, all placed together in a vertical container VStack and each component is given one version of the truth – the data model from SwiftData. Note the clarity and simplicity of the component StatisticsView on the screen above – all the complexity is decomposed and hidden in details that are invisible here.

Modifier .frame(height: 260) For the top pie chart, it provides an accurate height indication. Without him bagel from above will visually appear disproportionately small. If there were only two diagrams and not three, then this restriction would then be unnecessary. Therefore, frame should not be included inside the component SectorMarkView. By the way, let's look at this component in more detail:

Nothing special; SwiftUI, in the absence of other competitors for screen space, stretched the diagram to fill the entire screen. Let's mark the modifier .groupBoxed – it is not in the standard modifier library, it is a custom element. Here he is:

GroupBoxWrapper doesn't do anything super complicated, just packages our content view-source inside the container GroupBoxslightly simplifying the visual presentation of the code, but another principle of designing SwiftUI visual components is important here: we used the protocol ViewModifier And extension for the record Viewto add another custom Lego component to the library of standard components.

You probably know, but just in case: repeatable modifiers that appear on input fields, pictures and other standard elements can be grouped together also using ViewModifier:

In conclusion, perhaps the most interesting thing:

State driven application

SwiftUI makes it surprisingly easy to make a directly state-driven app.

For example: [бесплатное] application Swift-Way in the App Store – a simulator of recruiting Swift developers at A Dream Company. The application has a simple interface, but complex logic and scripts, includes a special scripting language with an interpreter for generating interview dialogues on a “live” screen, a model of programmer competence.

At any given time, this application can be in only one of four states:

enum AppState: String {
    case intro      // "вы получите приглашение" - сеттинг игры
    case interview  // диалоги: скрининг, HR, тех. интервью
    case menu       // главный экран с TabView
    case game       // встроенная игра, расслабиться в ожидании интервью
}

How does the application navigate to each of them? Let's look at ContentView – root view in the hierarchy:

First, we see that depending on the current value in the variable appState application using design switch displays one of three case with Views. The simplest one is MainView with the tab bar, which is now visible on the dark screen of the simulator on the right. This is a SwiftUI component, so the state variable is passed there through an ordinary binding.

The other two – the interview screen and the casual game screen – are another framework of the UIKit era (SpriteKit), so the state change here is made through notification of the notification center in the closure.

Whenever the current display mode terminates, it changes or signals a change in the state of the application and the root ContentView in this case, it updates the desired view in its hierarchy.

This is a simple and convenient diagram for developing the overall architecture of an application. So simple that it’s hard to even call it a “design pattern.”

However, here we need to add that if the application has complex business logic with stages, then it is necessary to distinguish between the state interface depending on the condition stories. In this example there is also additional state (variable situation) which tracks the overall status of the recruitment funnel of a new programmer to the development team. A status can have one initial state (an invitation has been received from the company for a series of interviews) and several final states, including a pleasant offer and various transitional statuses between them.

Depending on the status of the story, as well as temporary events (interviews take place in real time), completely different dialogues are formed using one visual component.

Summarize

There's no need to complicate things with patterns with unproven value – it's practical to use SwiftUI “as is”.

The presence of unit tests does not significantly change the principle of architecture – it arises necessity use protocols for the data model and there may be a special layer of abstractions, and if the architecture is magically transformed into MVVM – then it’s just magic or the art of interpretation 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *