hansontechsolutions.com

Mastering ViewState in SwiftUI: A Comprehensive Overview

Written on

Chapter 1 Understanding ViewState

In the realm of SwiftUI, views don't behave like traditional views. Instead, they represent lightweight descriptions of our intended user interfaces, created by combining application data with specific structures that dictate layout and behavior.

At the heart of this is the principle of a single source of truth, which suggests that each UI component should be driven by one definitive piece of data. When this data changes, the interface should reflect those changes seamlessly. This powerful principle can resolve many of the issues found in imperative programming.

However, challenges arise when an interface depends on multiple data points. How do we ensure that the definition we generate is accurate and the information displayed is correct? A somewhat tongue-in-cheek response would be to simply avoid such situations. Interestingly, this is indeed a valid approach.

Let's explore this concept with a familiar scenario: the Loading View.

Section 1.1 The Loading View Scenario

We all have experience creating loading views, and we understand the challenges involved. For instance, let's define a few requirements for a fictional banking application:

  • Call an API to retrieve a list of user accounts.
  • Display the accounts if the API call is successful (which is a different discussion).
  • If the API call fails, show an error message and allow the user to retry.
  • Present a progress view during loading, ideally with a modern shimmer effect.
  • If the API call is successful but no active accounts are found, inform the user accordingly.

Now, let's consider what we need to achieve this. What constitutes our source of truth?

If you propose that the view model serves as the source of truth, I concur. However, this leads us to another question: what data does the view model require to present to the view?

It's evident that we need to manage an array of accounts, an error message, an empty message, and an indicator for loading status. Additionally, we need a mechanism to load the accounts.

Here's a SwiftUI view model that encapsulates these requirements:

class ClassicSwiftUIViewModel: ObservableObject {

@Published var accounts: [Account] = []

@Published var loading: Bool = false

@Published var empty: String?

@Published var error: String?

let manager = AccountManager()

@MainActor

func load() async {

do {

accounts = []

empty = nil

error = nil

loading = true

accounts = try await manager.load()

if accounts.isEmpty {

empty = "No accounts found"

}

loading = false

} catch {

self.error = error.localizedDescription

loading = false

}

}

}

This model is a classic example, featuring published properties for each value we intend to display, along with an asynchronous load function to fetch our data.

Anyone who has spent a few days coding in SwiftUI or has watched a WWDC presentation would recognize this approach immediately. For simplicity, we are not injecting our AccountManager into the view model for this discussion, but feel free to apply your best practices.

#### Section 1.1.1 The View Implementation

struct ClassicSwiftUIView: View {

@StateObject var viewModel = ClassicSwiftUIViewModel()

var body: some View {

Group {

if let error = viewModel.error {

StandardErrorView(message: error) {

Task { await viewModel.load() }

}

} else if let empty = viewModel.empty {

StandardEmptyView(message: empty)

} else if viewModel.loading {

StandardProgressView()

} else {

AccountsListView(accounts: viewModel.accounts)

}

}

.navigationTitle("Accounts")

.onAppear {

Task { await viewModel.load() }

}

}

}

The logic of this view is convoluted, with checks for error messages, empty states, and loading indicators. This complexity can lead to potential pitfalls, especially if we forget to clear an error message before a new load attempt. Even with tests in place for the view model, we can only assume that the view logic will hold up.

Section 1.2 Simplifying with ViewState

A popular solution is to use an enumerated type for view states, defined as follows:

enum ViewState {

case loading

case loaded

case empty

case error

}

This enumeration clarifies the various states our view can assume, allowing us to streamline our view code:

struct ComputedStateView: View {

@StateObject var viewModel = ComputedStateViewModel()

var body: some View {

Group {

switch viewModel.state {

case .error:

StandardErrorView(message: viewModel.error) {

Task { await viewModel.load() }

}

case .empty:

StandardEmptyView(message: viewModel.empty)

case .loading:

StandardProgressView()

case .loaded:

AccountsListView(accounts: viewModel.accounts)

}

}

.navigationTitle("Accounts")

.onAppear {

Task { await viewModel.load() }

}

}

}

This approach is much cleaner, devoid of complex logic within the view. Now, we only need to ensure that our view model is functioning correctly.

#### Chapter 2 Advanced ViewState Management

The first video titled "5 Steps to Better SwiftUI Views" explores effective techniques for enhancing your SwiftUI views.

The second video, "Switching view states with enums – Bucket List SwiftUI Tutorial 3/12," dives into practical applications of enum-based state management in SwiftUI.

By implementing these strategies, you can improve the clarity and maintainability of your SwiftUI applications. The incorporation of enumerated states eliminates ambiguity and enhances your code's reliability, making your development process smoother and more efficient.

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

The Enigmatic Oort Cloud: The Outer Limits of Our Solar System

Discover the vastness of the Oort Cloud and its significance in the Solar System.

Understanding the Chemistry of Charred Foods and Cancer Risk

Exploring the relationship between charred foods, chemical reactions, and cancer risk through scientific insights.

Discovering the Real Benefits of Journaling: A Guide

Explore how to journal effectively to maximize its benefits for mental health and personal growth.

Transforming My Morning Routine: A Journey to Joy and Ease

Discover how changing my morning routine from obligation to enjoyment transformed my days and well-being.

Finding Depth in Dining: Revitalizing Your Eating Experience

Explore the deeper significance of meals and the importance of sharing food with others for a more fulfilling dining experience.

Achieving Goals: 7 Steps to Cultivate Iron Discipline

Discover seven effective steps to enhance self-discipline and achieve your goals, drawn from marathon training insights.

Embracing Our Alter Egos: The Roles We Play in Life

Explore the significance of our roles and alter egos in shaping our identities and enhancing our lives.

Why Android Phones Are the Superior Choice Over iPhones

Explore the compelling reasons that make Android phones a better option than iPhones, from customization to charging convenience.