Understanding the SwiftUI View Lifecycle and Data Management

Written by maxnechaev | Published 2024/01/09
Tech Story Tags: swiftui | lifecycle | swift | ios | ios-app-development | ios-development | combine | mobile-app-development

TLDRvia the TL;DR App

Introduction

Hi, everyone, my name is Maksim Nechaev. I'm a Senior iOS developer at Snoonu, and also the founder of my own startup. Like many other developers, I've written a lot of apps and features on UIKit. All the global projects I've worked with have been on it. And no wonder because it is the most common set of UI elements if we talk about iOS development. Along with UIKit, we should immediately mention layout via constraint. Often, we used SnapKit to simplify this task and write more minimalistic code.

But times were changing, and a new, trendy, and hip SwiftUI framework appeared on the horizon. At first, it was shunned. They looked at it with derision. Maybe it even made sense at first because SwiftUI had huge limitations and shortcomings. We couldn't do 50% of what we could do with UIKit. But years later, SwiftUI has become more powerful, so much so that many companies don't even look at UIKit when starting new projects. Why is that?

It's because SwiftUI has a declarative layout style, uses convenient and fast reactive elements, and building any screen complexity takes from 30 minutes to two hours, while on UIKit, you would have spent at least a day. And what is time for business? We all know time is money. So, the first thing SwiftUI is already doing right now is saving money for businesses.

Let's go deeper. What else is great about SwiftUI? The ease of creating animations. Yes, yes, if you imagine a cool app with 60+ frames per second that responds smoothly to taps, buttons shrink with animation, screens pop out of the right side of the screen, everything happens in beautiful and pleasing (for UX) animations, all of this is very quick and easy to do inside SwiftUI. You can do a lot of this in UIKit, too, to tell you the truth. But spend a lot more effort and time.

I hope I'm doing well in convincing you that SwiftUI is ready for full production development and that you should be learning it right now. That's why, with my first article on this topic, I'd like to touch upon the SwiftUI View lifecycle. How a "view" is created, where it is stored and how it is updated, I would like to talk about it all in this article. Let's get started!

SwiftUI View vs UIView

Let's start with the fact that UIView is created once in most cases. After that, we simply update its data. A SwiftUI View can be redrawn and created dozens of times within a single screen. This is very well illustrated in the following image. You may notice that when we use UIViewRepresentable, a structure that helps us use UIView inside SwiftUI, we create it once. But further, to update it, we pull updateUIView and update it as many times as needed.

SwiftUI View is created many times, while UIView is created once.

Storing data in SwiftUI View

Let's take a look at ways to store data inside a SwiftUI View. After all, you're probably already thinking, how do you store data if the View is constantly being recreated and redrawn?

In total, we can divide it into three parts.

First. Let and var, the classic use of data.

Second. @State and @StateObject. Already more SwiftUI approach

Third. @Enviroment and @EnviromentObject

Let

Absolutely classic usage; we can create some kind of constant that we pass externally and use inside our structure.

struct CustomView: View {
    let text: String

    init(text: String) {
        self.text = text
    }

    var body: some View {
        Text(text)
    }
}

The only thing to keep in mind here is that usually, when we create a constant, we assume that it will not change. The situation here is that when CustomView is changed and redrawn, the text constant can be changed. That is, it can be "bound" to the objects being changed.

Var

This is where it gets interesting. What if we want to change a variable inside a structure? We have always done it in a very simple way, if we talk about classes in UIKit.

var text: String

Let's imagine that we have made our text a variable. It seems that now we can change it. But no, Swift won't let us do that. Structures are more capricious and rigid than classes, and even more so in SwiftUI. There is only one way to try to change text, and that is to create a mutating function. But unfortunately, we will get an error here, too. It will look like this: "Cannot use mutating member on immutable value.” In general, the idea with an ordinary var in the structure is very doubtful, and you should think ten times why you should do it. After all, SwiftUI has other cool tools for working with values, which we will talk about next.

State

The situation with the State is fundamentally the opposite. It can and must be used inside structures if we want to work with the value and change it somehow.

struct CustomView: View {
    @State private var text: String

    var body: some View {
        Text(text)
    }
    
    func changeText() {
        text = "Hello, World!"
    }
}

That is, if we create a structure with some local variables to which we tie local logic, we should use @State, so that all internal Views that use this value will be updated. Everything will work correctly, but... there is always a "but". What if we want to set the value externally?

For example, like this

struct CustomView: View {
    @State private var text: String

    init(text: String) {
        self.text = text
    }

    var body: some View {
        Text(text)
    }
}

Do you think it will work, and in what cases? To be fair, it will work, but only when we set the first value. When we set all subsequent values, only the first one will be displayed inside the body.

Here, it is important to realize that @State does not live as long as View does. That is, when we create @State inside View, we can change it inside View as many times as we want. The view will be updated and redrawn, but the local value will be stored. But if at the same time we try to set new values for @State through the initializer, they will be simply ignored.

What is the output here? @State should be used only for saving and updating local data, but not those passed externally to the structure.

StateObject

Now, let's talk about using StateObject. In general, we use @StateObject to set some kind of ViewModel for our screen. That is, we pair it with a class that obeys the ObservableObject protocol. It works very similarly to @State, with one exception: it is created after the first initialization of the structure. That is, while @State could somehow be passed to init to put the first value, @StateObject won't work. It will be deleted from memory after the initializer is called and will be created again.

Environment

Let's move on to the consideration of @Environment. This property modifier is used to access data that is passed down the view hierarchy. Unlike @StateObject, which is used to control the state of an object within a single view, @Environment allows you to access data that is shared by the entire application or a specific part of it.

Let's say you have some value that needs to be available in many places in your interface, such as custom settings or a design theme. Instead of passing this value through all levels of the hierarchy, you can use @Environment for easier access.

When you mark a property in your SwiftUI view as @Environment,SwiftUI automatically looks up the value of that property in the nearest matching context and provides it to you. If the value changes, SwiftUI also automatically updates the views that use that value. This makes @Environment a very powerful tool for managing data in SwiftUI.

Example of use:

struct ContentView: View {
    @Environment(\.locale) var locale: Locale

    var body: some View {
        Text("Current localization: \(locale.identifier)")
    }
}

EnvironmentObject

Now, let's break down @EnvironmentObject. This property modifier is used to inject dependencies into SwiftUI views. Unlike @Environment, which is designed to access system settings or values defined at the application level, @EnvironmentObject is used to pass and use user data between views.

When you use @EnvironmentObject, you expect the object to be provided by the parent views. This allows you to create more modular and easily scalable applications because your views do not depend on specific implementations of their dependencies. Instead, they dynamically retrieve the necessary objects from their environment.

Example usage of @EnvironmentObject:

class UserSettings: ObservableObject {
    @Published var score = 0
}

struct ContentView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        Text("Score: \(settings.score)")
    }
}

SwiftUI View Life Cycle

Understanding the life cycle of a view in SwiftUI is key to creating efficient and well-performing applications. Let's explore this process in a detailed and interesting way.

1. Initialization (Initialization)

Every SwiftUI View starts its life journey with initialization. This is when SwiftUI creates an instance of your view and sets up its initial state. It's important to realize that SwiftUI views are structures, meaning they are value types. When you create a view, you are actually creating a "snapshot" of it at this point in time.

2. State Update (State Update)

After initialization, SwiftUI watches for changes in data that may affect your view. This data can include @State, @Binding, @ObservedObject, @EnvironmentObject, and @Environment. When one of these values changes, SwiftUI starts the process of updating the view.

3. Body Computation (Body Computation)

In this step, SwiftUI calls the body property of your view. body is a computed property, and every time SwiftUI detects a change that affects the view, it recomputes body. This process allows SwiftUI to determine which parts of the UI need to be updated.

4. Rendering & Layout (Rendering & Layout)

Once the body of the view has been computed, SwiftUI proceeds to the rendering and layout phase. In this stage, SwiftUI determines exactly how the interface elements should be arranged and rendered. This process involves calculating the dimensions, positions, and other layout aspects for each element on the screen.

5. View Activation (View Activation)

This is a specific point in the lifecycle that relates to user interaction with the view. For example, it could be the activation of a text box or the appearance of a view on the screen.

6. Deinitialization (Deinitialization)

The last step in the life cycle of a view is to deinitize it. When a view is removed from the SwiftUI view hierarchy, it is deinitialized. This is an important time to release resources or perform any cleanup.

Important Points:

  • Reuse: SwiftUI frequently recreates and reuses views. Therefore, it is important to avoid expensive operations in initializers and body.
  • Efficiency: SwiftUI is optimized to ensure that only the necessary parts of the interface are updated. This improves the performance and efficiency of the application.

Conclusions

SwiftUI View is very different from the familiar UIView, but that's the point. SwiftUI is different, and that's a good thing. In this article, I tried to convey the essence of working with SwiftUI View as succinctly and clearly as possible because this is a key skill for a SwiftUI developer. You will come across it in every task, which means it's worth understanding it very deeply. You need to be conscious of what you are writing and creating.

I wish everyone good luck on the path to becoming powerful developers. If you are reading this article, then you are already on the right path. Thanks for your time, bye everyone!

Please respond to this article so I can write more useful material for you.


Written by maxnechaev | Founder, Tech Lead, iOS Software developer
Published by HackerNoon on 2024/01/09