thespacebetweenstars.com

Streamlining SwiftUI View Code with ViewState and ViewBuilder

Written on

Chapter 1: Introduction

Imagine you've created a complex iOS application filled with lines of code, yet managing the view code has become quite cumbersome and challenging to navigate. Fortunately, we can enhance its readability!

Case Study: Todos App

// HomeView.swift

// TodosApp

//

// Created by Kevin Jonathan on 30/01/24.

//

import SwiftUI

struct HomeView: View {

@ObservedObject var presenter: HomePresenter

init() {

presenter = HomePresenter(networkService: NetworkService())

}

var body: some View {

VStack {

// Display your todos or error message based on presenter's state

if presenter.filteredTodos == nil {

VStack {

ProgressView()

Text("Loading..")

}

} else if let error = presenter.errorMessage {

Text("Error: (error)")

} else if let todos = presenter.filteredTodos, !todos.isEmpty {

List(todos, id: .id) { todo in

HStack {

Text(todo.title)

Spacer()

if todo.completed {

Image(systemName: "checkmark")

}

}

}

} else {

Text("No Todos")

}

}

.navigationTitle("Todos")

.onAppear {

self.presenter.fetchData()

}

.refreshable {

self.presenter.fetchData()

}

.searchable(text: $presenter.searchText)

}

}

Recognizing Complexity

Notice how adding more lines to our SwiftUI view body can complicate readability. In fact, the code might become even lengthier in your case. Mixing UI logic with data checks can lead to confusion. So, how do we address this issue?

Introducing View State

I refer to this approach as "view state," which allows us to control what the view displays conveniently. To start, we can define a ViewState enum to handle the view's state and create a HomePresenter.swift file for managing what is presented to the view. This concept is inspired by the VIPER architecture, though we won’t implement the entire pattern in this tutorial.

// ViewState.swift

// TodosApp

//

// Created by Kevin Jonathan on 30/01/24.

//

import Foundation

enum ViewState {

case loading

case empty

case loaded

case error(String)

}

// HomePresenter.swift

// TodosApp

//

// Created by Kevin Jonathan on 30/01/24.

//

import SwiftUI

class HomePresenter: ObservableObject {

@Published var todos: [Todo]?

@Published var errorMessage: String?

@Published var searchText: String = ""

var filteredTodos: [Todo]? {

get {

guard let todos = self.todos else { return nil }

guard searchText != "" else { return self.todos }

return todos.filter { $0.title.lowercased().contains(searchText.lowercased()) }

}

}

var viewState: ViewState {

get {

if filteredTodos == nil {

return .loading

} else if let error = errorMessage {

return .error(error)

} else if let todos = filteredTodos, !todos.isEmpty {

return .loaded

} else {

return .empty

}

}

}

// ...

}

With the presenter and its computed properties, managing the view state becomes straightforward! Now, we can use this property to dictate what the view should showcase. Let's adjust our View code accordingly:

// HomeView.swift

// TodosApp

//

// Created by Kevin Jonathan on 30/01/24.

//

import SwiftUI

struct HomeView: View {

// ...

var body: some View {

VStack {

// Display your todos or error message based on presenter's state

switch presenter.viewState {

case .loading:

VStack {

ProgressView()

Text("Loading..")

}

case .empty:

Text("No Todos")

case .loaded:

List(presenter.filteredTodos ?? [], id: .id) { todo in

HStack {

Text(todo.title)

Spacer()

if todo.completed {

Image(systemName: "checkmark")

}

}

}

case .error(let error):

Text(error)

}

}

// ...

}

}

Enhancing Readability with ViewBuilder

While the code is now simpler, readability can still be improved due to the numerous views present. Why not categorize these views for clarity? This is where ViewBuilder comes into play.

#### What Is ViewBuilder?

In SwiftUI, a ViewBuilder is a powerful tool that enhances code cleanliness and readability by enabling the creation of views through closures. When you see functions or initializers labeled with @ViewBuilder, it indicates that multiple views can be passed within curly braces, combining them into a single view seamlessly.

#### Implementation

To categorize the current HomeView code based on view states, we can modify it as follows:

// HomeView.swift

// TodosApp

//

// Created by Kevin Jonathan on 30/01/24.

//

import SwiftUI

struct HomeView: View {

// ...

var body: some View {

VStack {

switch presenter.viewState {

case .loading:

loadingView

case .empty:

emptyView

case .loaded:

loadedView

case .error(let error):

errorView(error: error)

}

}

// ...

}

}

// MARK: ViewBuilder

private extension HomeView {

@ViewBuilder

var loadingView: some View {

VStack {

ProgressView()

Text("Loading..")

}

}

@ViewBuilder

var emptyView: some View {

Text("No Todos")

}

@ViewBuilder

var loadedView: some View {

List(presenter.filteredTodos ?? [], id: .id) { todo in

HStack {

Text(todo.title)

Spacer()

if todo.completed {

Image(systemName: "checkmark")

}

}

}

}

@ViewBuilder

func errorView(error: String) -> some View {

Text(error)

}

}

Further Streamlining

To enhance readability even more, we can separate the loadedView from the main view and utilize a ViewBuilder for individual todo items:

@ViewBuilder

var loadedView: some View {

List(presenter.filteredTodos ?? [], id: .id) { todo in

todoItem(todo: todo)

}

}

@ViewBuilder

func todoItem(todo: Todo) -> some View {

HStack {

Text(todo.title)

Spacer()

if todo.completed {

Image(systemName: "checkmark")

}

}

}

Conclusion

The code is now significantly easier to comprehend! You can find the complete code in the GitHub repository linked below for reference.

This article was published in January 2024, and it's worth noting that the information may become outdated. I welcome any feedback regarding this implementation in the comments section below. I understand that this approach may not be perfect and can always be refined.

Thank you for reading!

Share the page:

Twitter Facebook Reddit LinkIn

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

Recent Post:

Exploring the Mysteries of White Holes in the Cosmos

Delving into the concept of white holes, their theoretical existence, and their relationship to black holes and the universe.

Keurig's Failed Cocktail Machine: Lessons Learned from Drinkworks

Analyzing the failures of Drinkworks, a cocktail machine by Keurig and AB InBev, reveals critical lessons on market assumptions and customer needs.

Understanding the Use of Ref Parameters in C#

Explore the concept of ref parameters in C#, their syntax, and practical examples to understand their significance in method calls.

Innovative MagSafe Accessory: Transforming Your iPhone Experience

Discover the revolutionary MagSafe accessory that enhances your iPhone experience without the hassle of traditional mounts.

Create Stunning Animated Charts with Pandas Alive Library

Discover how to create engaging animated charts using the Pandas Alive library with just a single line of code.

Exploring the Impact of Blackrock on Bitcoin and Crypto Trends

A deep dive into the implications of Blackrock's influence on Bitcoin and recent crypto events, including thefts and corporate moves.

Creating a Winning Social Media Strategy for 2024

Learn how to craft an effective social media strategy to enhance your brand's online presence and achieve your marketing goals.

# Discovering Purpose and Clarity: The Transformative Power of Healing

Explore how healing can redefine your career goals and personal purpose through self-awareness and transformation.