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:
loadingViewcase .empty:
emptyViewcase .loaded:
loadedViewcase .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!