Skip to content

Instantly share code, notes, and snippets.

@adam-zethraeus
Created November 12, 2024 16:42
Show Gist options
  • Select an option

  • Save adam-zethraeus/25e0c57809b658e03e312af7c27b4ef2 to your computer and use it in GitHub Desktop.

Select an option

Save adam-zethraeus/25e0c57809b658e03e312af7c27b4ef2 to your computer and use it in GitHub Desktop.
A SwiftUI view which catches and display subview errors. Similar to an error boundary in React.
public import SwiftUI
public struct CatchingView<Content: View, ErrorMessage: View>: View {
public init(
@ViewBuilder content: @escaping () throws -> Content
) where ErrorMessage == Text {
self.init {
Text("Any error occurred while attempting to load the content.")
} content: {
try content()
}
}
public init(
errorMessage: @escaping () -> ErrorMessage,
@ViewBuilder content: @escaping () throws -> Content
) {
let errorLog = ErrorLog()
self.errorLog = errorLog
self.errorMessage = errorMessage
self.builder = content
/// Note: this implementation is dependent on the difference between a 'view' in the SwiftUI
/// attribute graph and an instance of a SwiftUI 'View' implementation.
/// - The former represents a rendering on the screen and its associated state.
/// - The latter only defines a potential rendering's behavior.
///
/// `_instance` is a non-optional `State<Result<Content, any Error>>` property that is always
/// initialised by the system.
///
/// `self.state` is a reference to `_state`'s optional content.
///
/// By checking for `_state`'s optional contents we can determine whether the view has been
/// initialised before — and add its content only during the first initialisation.
if self.instance == nil {
_instance = .init(wrappedValue: Result {
do {
return try builder()
} catch {
errorLog.append(error: error)
throw error
}
})
}
}
// the content closure at the last time the view — i.e. view definition struct — was initialized.
private var builder: () throws -> Content
// the error log instance created in the first initialization of the view definition struct.
@State private var errorLog: ErrorLog
// the last result of trying to run a passed content closure.
@State private var instance: Result<Content, any Error>?
// the error message to display on failure, as last passed to this view definition struct.
private let errorMessage: () -> ErrorMessage
struct InitializationFailure: LocalizedError {
var errorDescription: String? { localizedDescription }
var localizedDescription: String {
"""
CatchingView should have attempted to initialize \(Content.self) when created — but did not.
This should never happen.
"""
}
}
@Observable
final class ErrorLog {
struct Entry: Identifiable {
let id = UUID()
let error: any Error
}
private(set) var log: [Entry] = []
func append(error: any Error) {
log.append(Entry(error: error))
}
}
var currentFailure: any Error {
switch instance {
case .failure(let error):
error
default:
InitializationFailure()
}
}
public var body: some View {
switch instance {
case .success(let content):
content
case .none, .failure:
ContentUnavailableView {
errorMessage()
} description: {
List {
Section {
LabeledContent(String(describing: currentFailure)) {
Text(currentFailure.localizedDescription)
.font(.body.monospaced())
}
} header: {
Text("Error Details")
}
Section {
ForEach(errorLog.log) { entry in
LabeledContent(String(describing: entry.error)) {
Text(entry.error.localizedDescription)
.font(.footnote.monospaced())
}
}
} header: {
Text("Previous Errors")
}
}
.listStyle(.plain)
} actions: {
Button {
instance = Result {
do {
return try builder()
} catch {
errorLog.append(error: error)
throw error
}
}
} label: {
Label("Retry", systemImage: "arrow.clockwise")
}
.buttonStyle(.borderedProminent)
}
}
}
}
@adam-zethraeus
Copy link
Author

License: MIT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment