A SwiftUI view that emits asynchronous loading states: .idle, .loading, .loaded, and .failure.
@State private var loader = BlockLoadable {
try await fetchData()
}
var body: some View {
LoadingView(loader: loader) { value in
Text("Success loading \(value)")
}
.emptyView {
Text("Nothing here.")
}
.errorView { error in
Text("Error: \(error.localizedDescription)")
}
.progressView { progress in
ProgressView("\(progress.percent)% loaded")
}
}Use default loading and error views or pass your own. Data is loaded from a type conforming to the Loadable protocol. A few convenient implementations are provided:
- BlockLoadable: loads data from the closure provided.
- ConcurrencyLimitingLoadable: executes async calls limiting the number of concurrent operations.
- DebouncingLoadable: delays execution until an elapsed time since the last event.
- RetryableLoader: retry logic and exponential backoff.
Loaders are composable - you can nest them at will to combine behaviors like retry, debounce, and concurrency limiting. Example:
let searchLoadable = SearchableLoadable()
let debounced = await DebouncingLoadable(
wrapping: searchLoadable,
debounceInterval: 0.3
)
let retryable = RetryableLoader(
base: debounced,
maxAttempts: 2
)
let finalLoader = ConcurrencyLimitingLoadable(
wrapping: retryable,
concurrencyLimit: 1
)Initial loading happens automatically, to prevent it pass loadOnAppear: false and call loader.load() later:
LoadingView(loader: loader, loadOnAppear: false) { value in
...
}This library uses Swift 6. The v26 branch demonstrates usage of the native Observations framework available in iOS 26+.
iOS 26+, macOS 26+, Xcode 16
Using Xcode Swift Package Manager:
- In Xcode, select File > Add Packages...
- Enter URL:
https://github.com/janodevorg/LoadingView.git - Select the
LoadingViewlibrary and add it to your target.
.package(url: "git@github.com:janodevorg/LoadingView.git", from: "1.0.3"),
.product(name: "LoadingView", package: "LoadingView"),To develop locally clone the repository and initialize submodules:
git clone https://github.com/janodevorg/LoadingView.git
cd LoadingView
git submodule init
git submodule update- BasicLoadingDemo: default behavior.
- CancellationDemo: cancelling a loading task.
- ConcurrencyLimitingDemo: executing n operations with a concurrency limit.
- CustomViewsDemo: providing custom views for empty, loading, success, and failure state.
- DebouncedDemo: executing an operation when a certain time elapses since a given event.
- ErrorScenariosDemo: different error types and custom error handling views with retry capabilities.
- ManualLoadingDemo: compares automatic loading on appear versus manual loading triggered by user action.
- MultipleLoadersDemo: demonstrates managing multiple independent loaders within a single view.
- ProgressTrackingDemo: shows real-time progress updates during long-running operations.
- RetryDemo: automatic retry functionality with configurable attempts for handling transient failures.
See them in action in the Demo application.
Wrap any Loadable with RetryableLoader to automatically handle transient errors.
import LoadingView
import SwiftUI
// A loader that is designed to fail twice before succeeding
@State private var flakeyLoader = FlakeyLoader(successAfterAttempts: 3)
// Wrap it with RetryableLoader
@State private var retryableLoader = RetryableLoader(
base: flakeyLoader,
maxAttempts: 5
)
// Use it in your view
LoadingView(loader: retryableLoader) { message in
Text(message)
}Wrap your search loader with DebouncingLoadable to prevent sending a network request on every keystroke.
import LoadingView
import SwiftUI
// A loader that performs a search
@State private var searchLoader = DebouncedSearchLoader()
// Wrap it with DebouncingLoadable
@State private var debouncedLoader = await DebouncingLoadable(
wrapping: searchLoader,
debounceInterval: 0.5 // 500ms
)
// In your view...
TextField("Search...", text: $searchText)
.onChange(of: searchText) { newValue in
searchLoader.searchText = newValue
Task {
// This load() call is debounced
await debouncedLoader.load()
}
}
LoadingView(loader: debouncedLoader) { results in
List(results, id: \.self) { Text($0) }
}There are some docs in the Docs/ folder.
- Documentation.md: API reference and usage guide.
- DesignDecisions.md: how the library changed to overcome several pitfalls.
- Troubleshooting.md: Find solutions to common problems and learn about best practices.
- AgentGuide.md: Complete technical reference optimized for AI agents.
Tip for AI-assisted development: If you encounter an issue while using this package with an AI coding agent, provide it with the relevant documentation files above. The docs contain detailed troubleshooting patterns and anti-patterns that help agents quickly identify and fix common problems.