Created
November 12, 2024 16:42
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| } | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
License: MIT