Oversimplifying SwiftUI View Code using ViewState and SwiftUI’s ViewBuilder (Case Study Included!)

Kevin Jonathan
6 min readJan 30, 2024

Let’s say you have developed a pretty complex iOS app with a lot of lines of code, but you are having a hard time to manage the view code because it’s quite messy and hard to read. We can definitely oversimplify it to make it more readable!

Oversimplify? Why?

Why I mentioned “Oversimplify”? Because I removed almost everything (including the use of generics, the complete design pattern, etc) and only included the very basic implementation that you can expand/adjust later on, depending on your needs. Perhaps, I went too far about this oversimplifying. But, don’t worry, I have included the Github repository below for the complete working example app!

Case Study: Todos App

For this article, I will be using a sample todos app that can fetch the todos data from https://jsonplaceholder.typicode.com/ and display the data in a SwiftUI view for the case study. Keep in mind that I am trying to make this app’s code as simple as possible to help everyone understand better how this app’s code simplification works. This is the code snippet for the view part, provided below.

Update (22 February 2024): I noticed that many people in the comment section criticized my code for being too confusing/having wrong usages (Such as the way the state is being driven, the lack of generic usage, or the ObservedObject property wrapper being used wrongly). Keep in mind that I am only focusing on the very basic way to utilize enum and computed property to achieve something such as “oversimplifying” our SwiftUI view. So I won’t focus on the other aspects to avoid making the code overcomplicated. You can still improve the code according to your own knowledge of iOS/SwiftUI.

//
// 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)
}
}

Or the repository starting point:

Notice that every time we add new lines of code in our SwiftUI View body, we actually made the code much harder to read. It’s still quite a lot of codes in the SwiftUI body (In your case, it could be more than this). We also sometimes mix the logic for the data and the UI there (with the data checking for the view state, etc.). Then what should we do?

Introducing View State

I usually call this view state, because we can control what the view shows. This is pretty convenient, I would say.

First, we can start by creating ViewState enum to manage the view state later on, and then next, we can create a presenter file called HomePresenter.swift, to manage what should be presented to the view. This is a reference to the VIPER pattern actually. But I won’t be using the full pattern for the sake of 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()) }
}
}

// We need this to manage the view state better
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 (combined with the computed property of the view state), we can easily manage the view state now!

Now, we can refer to the computed property to determine what the view should display. So let’s change the code in View to reflect to this view state property.

//
// 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)
}
}
// ...
}
}

Oh it’s now definitely simpler and easier to read than before (with the view state, we don’t need to check the data in the UI anymore). But I think it’s still hard to read since the views are so many.

Introducing ViewBuilder

If there are too many different views in a SwiftUI view body, why not just separate the views by category so that it will be easier to read? Great idea. We can utilize ViewBuilder to achieve this.

What Is ViewBuilder?

So, in SwiftUI, a ViewBuilder is like a cool helper (we usually call it wrapper) that we can utilize to organize our view. It's basically a way to erase the type of the view so that it has a single type when returned. When you see functions or initializers marked with @ViewBuilder, it means you can pass in multiple views inside curly braces, and it magically turns them into a single view.

Implementation

Let’s say we want to separate the current code in HomeView based on the view states, we can change the code to be like this.

//
// 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:
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)
}
}

Woah it’s now easier to read, can we separate a view from a ViewBuilder to a new separate ViewBuilder? Of course! For example, we might want to separate the todos from the loadedView. We can use a ViewBuilder for this.

// ...
@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")
}
}
}
// ...

Now, it’s easier to read the code right?

Full Project Code

You can find the full code in the Github Repository below for references.

That’s all you need to know to oversimplify your messy SwiftUI view code! Do keep in mind that this article is published on January 2024, and could be outdated in the future.

Also, I would appreciate it if you have any input about this implementation in the comment section below. I know that this idea of mine isn’t perfect and could be improved.

Thank you for reading!

--

--

Kevin Jonathan

Just a student intricately weaving personal life experience and technology related stuffs, currently navigating the intersections of life.