How to show a full screen Error View in SwiftUI?

Ekaterina Temnogrudova
4 min readMar 31, 2023

--

Error handling in SwiftUI is essential to creating a seamless user experience. There are many tutorials about error handling in SwiftUI apps. Most of them describes a very common practice to show errors. However I didn’t find an acceptable solution to present a full screen Error View for different views, but to handle errors individually depending on a parent view purpose with the same business layer design. Let’s begin with the code example below of how an error can be presented using an Alert as a simple solution.

SwiftUI Error Handling, using an Alert

First of all we need to create a Network.swift file, in which we will call the API.

import Combine
import Foundation

class Network: ObservableObject {

@Published var items: [Item] = []
@Published var hasCallAPiError = false
private var cancellables = Set<AnyCancellable>()

func getData() {
let url = URL(string: "https://yoururl.com/model")
URLSession.shared.dataTaskPublisher(for: url!)
.map { $0.data }
.decode(type: Model.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.hasCallAPiError = true
case .finished:
break
}
}, receiveValue: { Model in
// More code...
})
.store(in: &cancellables)
}
}

Now we add this Network class as an EnvironmentObject in the ProjectNameApp.swift file.

import SwiftUI

@main
struct ProjectNameApp: App {
var network = Network()

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(network)
}
}
}

In ContentView add Network as an EnvironmentObject and make a request onAppear of the view. Requesting can result in an error, which we want to present in the alert accordingly.

import SwiftUI

struct ContentView: View {
@EnvironmentObject var network: Network

var body: some View {
VStack() {
// More code...
}
.onAppear {
network.getData()
}
.alert("Error message", isPresented: $network.hasCallAPiError) {
Button("Retry", role: .cancel) {
network.getData()
}
}
}
}

An advantage of this approach is the default behavior of the alert. It allows to dismiss the alert after clicking the button and retry to make a proper request from different parent views in the same way.

Meanwhile the most companies have a standard way to show errors. Actual apps might come up with a more sophisticated UI, for example, to substitute the whole view for an error view.

SwiftUI Error Handling, using an ErrorView

One of the solutions, how to show a full screen Error View, we can find in this article:

Shortly, the idea here is to show or not a view that will overlay the entire screen showing the error and how to handle that.

struct ContentView: View {
@EnvironmentObject var network: Network

var body: some View {
VStack() {
// More code...
if let error = network.hasCallAPiError { // << error handling here
CustomServerError(network: network)
}
}
.onAppear {
network.getData()
}
}
}


struct CustomServerError: View {
@ObservedObject var network: Network

var body: some View {
// More code...
Button("Reload") {
network.getData()
}
}
}

The problem with this approach is that it doesn’t allow to a CustomServerError View to be used for other Views with different requests API to handle errors in the same fashion. So let’s improve the first basic solution to define how to do it, using sheets in SwiftUI.

SwiftUI Error Handling, using a Sheet

The main idea of my solution is to pass events from the CustomServerError View to parent views, where to handle it differently but with the same business layer design. On the one hand, it helps to handle errors in the same way in the whole app, and on the other hand, it allows not to create unnecessary single-purpose error views. Let’s go over the solutions I’ve written.

First of all I defined possible actions:

enum ProjectAction {
case retry
case exit
}

Then I defined a closure actionPerformed in the CustomServerError View. I’m calling this closure with action type, which is performed, on each button action. Also you can pass some other values like id or something else via the same closure.

struct CustomServerError: View {
var actionPerformed: ((ProjectAction) -> Void)?

var body: some View {
VStack(){
//More code...
Button(action: {
actionPerformed?(.retry)
}, label: {
Text("Retry")
})
Button(action: {
actionPerformed?(.exit)
}, label: {
Text("Exit")
})
}
.ignoresSafeArea()
.navigationBarHidden(true)
}
}

In the ContentView, which is the parent view, I defined a function actionPerformed, where we can make request again. When we create the CustomServerError View, we just pass this function there.

import SwiftUI

struct ContentView: View {
@EnvironmentObject var network: Network

var body: some View {
VStack() {
// More code...
}
.onAppear {
network.getData()
}
.sheet(isPresented: $network.hasCallAPiError) {
CustomServerError(actionPerformed: actionPerformed)
}
}

private func actionPerformed(action: ProjectAction) {
switch action {
case .retry:
network.hasCallAPiError = false
network.getData()
case .exit:
print("exit")
}
}
}

Conclusion

Managing all kinds of error views can be frustrating if we have to present them all individually. By creating a universal business logic solution, we allow ourselves to simplify the implementation. This solution determines whether we can show the error in the sheet, so we only have to care about setting up a binding between the parent view and the potential errors.

Thanks for reading! All feedbacks are appreciated.

--

--