Skip to content

Instantly share code, notes, and snippets.

@mbernson
Last active November 24, 2025 12:48
Show Gist options
  • Select an option

  • Save mbernson/109443fa676514b3be506d5ede1df475 to your computer and use it in GitHub Desktop.

Select an option

Save mbernson/109443fa676514b3be506d5ede1df475 to your computer and use it in GitHub Desktop.
A view that loads asynchronous content and displays it. It supports a loading state and an error state with a retry button.
import SwiftUI
enum AsyncContentState<Data> {
case loading
case loaded(Data)
case error(Error)
}
struct AsyncContentView<Data, Placeholder: View, Content: View>: View {
@State private var state: AsyncContentState<Data> = .loading
var load: () async throws -> Data
@ViewBuilder var content: (Data) -> Content
@ViewBuilder var placeholder: () -> Placeholder
var body: some View {
Group {
switch state {
case .loading:
placeholder()
case let .loaded(data):
content(data)
case let .error(error):
ErrorView(error: error) {
state = .loading
await loadContent()
}
}
}
.task {
await loadContent()
}
}
private func loadContent() async {
do {
let data = try await load()
withAnimation {
state = .loaded(data)
}
} catch {
// Filter cancellation errors - don't show error UI
if error is CancellationError {
return
}
withAnimation {
state = .error(error)
}
}
}
}
extension AsyncContentView {
init(
load: @escaping () async throws -> Data,
@ViewBuilder content: @escaping (Data) -> Content
) where Placeholder == DefaultAsyncContentProgressView {
self.load = load
self.content = content
placeholder = { DefaultAsyncContentProgressView() }
}
}
struct DefaultAsyncContentProgressView: View {
var body: some View {
ProgressView()
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ErrorView: View {
let error: Error
let retry: () async -> Void
var body: some View {
ContentUnavailableView {
Text(.errorGenericOccurred)
} description: {
Text(error.localizedDescription)
} actions: {
Button {
Task {
await retry()
}
} label: {
Label("Retry", systemImage: "arrow.clockwise")
}
}
.padding(.spacingSmall)
}
}
#Preview("Basic example") {
AsyncContentView {
try await Task.sleep(for: .seconds(1.0))
if Bool.random() {
return "Hello, World!"
} else {
throw URLError(.badServerResponse)
}
} content: { data in
Text(data)
.padding()
}
}
#Preview("Custom placeholder") {
AsyncContentView {
try await Task.sleep(for: .seconds(1.0))
if Bool.random() {
return "Hello, World!"
} else {
throw URLError(.badServerResponse)
}
} content: { data in
Text(data)
.padding()
} placeholder: {
Text(verbatim: "Hello, World!")
.redacted(reason: .placeholder)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment