Skip to content

About

Loading, error, and success state presentation for SwiftUI and UIKit.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

1,028 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DMUnLoader

A HUD for the loading, success and failure states of an iOS app, for SwiftUI and UIKit.

CI Swift 6.2+ Platforms License: MIT FOSSA Status Commit activity Last commit DeepSource Codecov

DMUnLoader: loading, success and failure for iOS, in SwiftUI and UIKit

What it is

DMUnLoader shows one HUD at a time over a scene of your app: a spinner while work runs, then a success, or a failure with Close and an optional Retry. A loading manager holds the state, and your code calls it: showLoading, showSuccess, showFailure and hide. The HUD is drawn in a window of its own above the scene, and while it is shown it takes the touches of the scene, so the user waits for the result. The views of the states are the library's, with your settings, or your own.

Two phone screens, the default settings on the left and custom settings on the right. A tap shows the loading HUD over the blurred screen, a card with a spinner and the loading text, yellow on the right, and then a success card with a checkmark and the text Successfully completed, which hides by itself.

Use it when an operation blocks the screen: a sign-in, a payment, a save that the user waits for before going on.

When not to use it:

  • For progress that must not block the screen. While a HUD is shown, the content under it receives no touch: put a ProgressView in your view instead.
  • For several operations at once, each with its own progress: a scene shows one state at a time.
  • If your app must not contain a private API of the system. The default backdrop is the variable blur of DMVariableBlurView, which uses one, and DMUnLoader depends on DMVariableBlurView, so its code stays in your app whatever backdrop you choose. See Backdrop.

Requirements

  • Swift 6.2 or later, which is Xcode 26.0 or later.
  • iOS 17 or later.

What each version is verified with:

What How
iOS 26.5 and 26.2 (Xcode 26.6) the tests of the package run on simulators in CI
iOS 26.5 (Xcode 26.6) and 18.6 (Xcode 26.3) the tests of the example app, hosted and UI tests, run on simulators in CI
iOS 17.5, 18.6 and 26.5 (Xcode 27.2) the tests of the package and of the example app ran on simulators for 1.2.0; before each release the package suite runs locally on iOS 17.5 and 18.6, and the example app on iOS 17.5
iPadOS 26.2 (Xcode 27.2) the tests of the example app, two scenes included, run on an iPad simulator before each release
Swift 6.2 (Xcode 26.0.1) CI compiles the library with that compiler; no test runs with it

Several windows on an iPad, beyond the two scenes that the tests of the example app use, are not tested.

Installation

Swift Package Manager

In Xcode, choose File > Add Package Dependencies and enter https://github.com/nikolay-dementiev/DMUnLoader. In a package manifest:

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [.iOS(.v17)],
    dependencies: [
        .package(url: "https://github.com/nikolay-dementiev/DMUnLoader.git", from: "1.2.0")
    ],
    targets: [
        .target(
            name: "MyApp",
            dependencies: [
                .product(name: "DMUnLoader", package: "DMUnLoader")
            ]
        )
    ]
)

DMUnLoader brings DMAction and DMVariableBlurView with it. The libraries that only its tests use are not fetched for your app.

CocoaPods

pod 'DMUnLoader', '1.1.0'

Version 1.1.0 is the last release planned for the CocoaPods trunk: the CocoaPods post of 30 November 2024 announces that the trunk stops accepting new podspecs on 2 December 2026. Later releases come through Swift Package Manager only. The podspec stays in the repository, and CI lints it.

Quick start

import DMUnLoader
import SwiftUI

@main
struct QuickStartApp: App {
    @StateObject private var loadingManager = DMLoadingManagerMain()

    var body: some Scene {
        WindowGroup {
            DMRootLoadingView(manager: loadingManager) { manager in
                LoadButton(loadingManager: manager)
            }
        }
    }
}

struct LoadButton: View {
    let loadingManager: DMLoadingManagerMain
    private let provider = DefaultDMLoadingViewProvider()

    var body: some View {
        Button("Load") {
            loadingManager.showLoading(provider: provider)
            Task {
                do {
                    let message = try await loadMessage()
                    loadingManager.showSuccess(message, provider: provider)
                } catch {
                    loadingManager.showFailure(error, provider: provider)
                }
            }
        }
    }
}

// A stand-in for your own work.
func loadMessage() async throws -> String {
    try await Task.sleep(for: .seconds(1))
    return "Loaded"
}

A tap shows the spinner, then the success, which hides by itself after 2 seconds.

Usage

SwiftUI with a manager your app owns

DMRootLoadingView(manager:content:), as in the quick start, shows the HUD of the manager you give it over the scene of the window the view is in. It needs no app delegate of DMUnLoader. Keep the manager alive outside the view, as the @StateObject does, and use one such view per scene: the HUD of each view joins the scene of that view. Another manager given in a later update takes over the HUD. When the view moves to another window, its HUD moves with it; a window that the host moves to another scene is not followed.

Observation

DMObservableLoadingManager is for an app that keeps its state with Observation (@State, the environment) and not with ObservableObject. It wraps a DMLoadingManagerMain. The manager stays the one owner of the state: the HUD still observes it as an ObservableObject, its $loadableState publisher is unchanged, and a manager of your own is untouched, because the facade wraps DMLoadingManagerMain only. The facade holds the manager, shows its loadableState as a property that Observation tracks and that you cannot set, and forwards showLoading(provider:), showSuccess(_:provider:), showFailure(_:provider:onRetry:) and hide() to it. Give facade.manager to DMRootLoadingView(manager:content:), and read loadableState on the facade: a view that reads it on the manager is not updated, because Observation does not track the manager.

import DMUnLoader
import SwiftUI

@main
struct ObservationApp: App {
    @State private var loading = DMObservableLoadingManager(manager: DMLoadingManagerMain())

    var body: some Scene {
        WindowGroup {
            DMRootLoadingView(manager: loading.manager) { _ in
                ObservationContent(loading: loading)
            }
        }
    }
}

struct ObservationContent: View {
    let loading: DMObservableLoadingManager

    var body: some View {
        VStack {
            Text(verbatim: loading.loadableState.rawValue)

            Button("Load") {
                loading.showLoading(provider: DefaultDMLoadingViewProvider())
            }
        }
    }
}

A view that reads loadableState is evaluated again when the state changes, wherever the change comes from: the operations of the facade, those of the manager, or its auto-hide. Several facades may wrap one manager, and each follows it. Everything runs on the main actor.

  • Observation is not synchronisation and not a lossless stream of events. Measured with Xcode 27.1: showing a state that equals the current one again (the loading state of the same provider twice, or hide() when nothing is shown) did not notify, while the loading state of another provider and the hide() of a shown HUD did. If you need to see every change, subscribe to the $loadableState publisher of the manager, which is unchanged.
  • Where to make the facade. Measured with an app built by Xcode 27.1 on iOS 17.5, 18.6 and 26.5: a parent view that was rebuilt six times made one facade, one manager and one subscription for a child view with @State private var loading = DMObservableLoadingManager(manager: DMLoadingManagerMain()). The SDK decides how often the initial value of @State is evaluated, and no other SDK was measured. To not depend on it, make the facade once, in your App struct, which SwiftUI creates once, and pass it down, as the example above does.
  • UIKit. A view controller that read loadableState in viewWillLayoutSubviews() was laid out again, with no call to setNeedsLayout(), when the state changed: on iOS 26.5 always; on iOS 18.6 only when the Info.plist of the app has UIObservationTrackingEnabled set to YES; on iOS 17.5 never, with the key or without it. Nothing else of UIKit is promised. Where UIKit does not track, call withObservationTracking yourself, which fired on all three systems, or keep using the $loadableState publisher.
  • withObservationTracking runs its onChange before the state changes. Measured on iOS 17.5, 18.6 and 26.5: a read of loadableState inside onChange still gave the previous state, and a read after the call that changed it gave the new one. So do not render from inside onChange: register again, and read, in a later turn of the main actor, as the snippet below does. Do not change the manager from inside onChange either, because the change is still in progress.
import DMUnLoader
import Observation

@MainActor
func followLoadableState(of loading: DMObservableLoadingManager, render: @escaping @MainActor (DMLoadableType) -> Void) {
    withObservationTracking {
        render(loading.loadableState)
    } onChange: {
        Task { @MainActor in
            followLoadableState(of: loading, render: render)
        }
    }
}

SwiftUI with the app delegate of DMUnLoader

DMRootLoadingView(content:) creates the loading manager itself and passes it to your content. It needs the app delegate: DMAppDelegateType, or DMAppDelegate<YourManager> for a manager type of your own, puts the scene delegate of DMUnLoader into the environment. Without it SwiftUI stops the app when the view appears.

import DMUnLoader
import SwiftUI

@main
struct ExampleApp: App {
    @UIApplicationDelegateAdaptor private var delegate: DMAppDelegateType

    var body: some Scene {
        WindowGroup {
            DMRootLoadingView { loadingManager in
                ContentView(loadingManager: loadingManager)
            }
        }
    }
}

struct ContentView: View {
    let loadingManager: DMLoadingManagerMain
    private let provider = DefaultDMLoadingViewProvider()

    var body: some View {
        Button("Show success") {
            loadingManager.showSuccess("Data loaded", provider: provider)
        }
    }
}

UIKit

The UIKit integration installs DMSceneDelegateTypeUIKit as the scene delegate. DMSceneDelegateHelper supplies the root view controller and receives the loading manager. The app declares a scene manifest, UIApplicationSceneManifest in its Info.plist: without one, UIKit does not ask the app delegate for a scene configuration.

import UIKit
import DMUnLoader

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        configurationForConnecting connectingSceneSession: UISceneSession,
        options: UIScene.ConnectionOptions
    ) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(
            name: "Default Configuration",
            sessionRole: connectingSceneSession.role
        )
        configuration.delegateClass = DMSceneDelegateTypeUIKit<AppDelegateHelper>.self
        return configuration
    }
}

@MainActor
struct AppDelegateHelper {}

extension AppDelegateHelper: @MainActor DMSceneDelegateHelper {
    static func makeUIKitRootViewHierarhy<LM: DMLoadingManager>(
        loadingManager: LM
    ) -> UIViewController {
        LoadingViewController(loadingManager: loadingManager)
    }
}

final class LoadingViewController<LM: DMLoadingManager>: UIViewController {
    private let loadingManager: LM
    private let provider = DefaultDMLoadingViewProvider()

    init(loadingManager: LM) {
        self.loadingManager = loadingManager
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        return nil
    }

    override func loadView() {
        let button = UIButton(type: .system)
        button.setTitle("Show success", for: .normal)
        button.addTarget(self, action: #selector(showSuccess), for: .touchUpInside)
        view = button
    }

    @objc
    private func showSuccess() {
        loadingManager.showSuccess("Data successfully loaded!", provider: provider)
    }
}

The requirement keeps its released name, makeUIKitRootViewHierarhy, until 2.0.0. DMUnLoader calls it on the main actor, so make the helper a @MainActor type that conforms with @MainActor DMSceneDelegateHelper, as above: in Swift 6 mode a plain conformance stops with "sending 'loadingManager' risks causing data races". The scene delegate creates its loading manager with init(), so with DMLoadingManagerMain it has the default settings.

Showing the states

  • showLoading(provider:) shows the spinner until the next state.
  • showSuccess(_:provider:) shows a message: any DMLoadableTypeSuccess, a String included.
  • showFailure(_:provider:onRetry:) shows a failure. The library's error view shows the title of the error settings, the error's localizedDescription, Close, and Retry when onRetry is given. showFailure(_:provider:) shows a failure without Retry.
  • hide() removes the HUD.

Retry runs your action and leaves the HUD as it is, so the action shows the next state itself. By default a failure with Retry also hides by itself after 2 seconds and on any tap; The loading manager's settings shows how to keep it until the user acts.

import DMUnLoader
import Foundation

@MainActor
final class DownloadModel {
    private let loadingManager: DMLoadingManagerMain
    private let provider = DefaultDMLoadingViewProvider()

    init(loadingManager: DMLoadingManagerMain) {
        self.loadingManager = loadingManager
    }

    func downloadFailed(with error: any Error) {
        let retry = DMButtonAction { [weak self] in
            self?.download()
        }
        loadingManager.showFailure(error, provider: provider, onRetry: retry)
    }

    func download() {
        loadingManager.showLoading(provider: provider)
        // Start the work again here.
    }
}

The provider of a state decides its views: DefaultDMLoadingViewProvider draws the library's views, and Custom views shows a provider of your own.

Configuration

The loading manager's settings

DMLoadingManagerDefaultSettings holds the settings of a DMLoadingManagerMain. Each one defaults to the behaviour of every release before 1.1.0:

  • autoHideDelay: how long a success or a failure stays. 2 seconds.
  • hudDismissal: how each kind leaves the screen, by itself or by a tap on its card or outside it, for a success, a failure without Retry and a failure with Retry. By default each hides after autoHideDelay and on any tap. The loading HUD stays until the next state, and taps do nothing.
  • hudWindowLevel: the level of the HUD window. .normal.
  • backdrop: what the HUD draws behind its card. .variableBlur; see Backdrop.
  • onBackdropFailure: told when the variable blur of the backdrop could not be drawn. None; see Backdrop.
import DMUnLoader

@MainActor
func makeLoadingManager() -> DMLoadingManagerMain {
    DMLoadingManagerMain(
        state: .none,
        settings: DMLoadingManagerDefaultSettings(
            autoHideDelay: .seconds(4),
            hudDismissal: DMHUDDismissalRules(
                failureWithRetry: DMHUDDismissal(autoHide: .never, cardTapHides: false)
            ),
            backdrop: .dim()
        )
    )
}

Here a failure with Retry waits for the user: it leaves through Close, a tap outside its card, or another state. Give such a manager to DMRootLoadingView(manager:content:). The settings of a view provider do not change the manager: its loadingManagerSettings is not read. The README of 1.0.x set the delay there, which never had an effect.

Backdrop

While a HUD is shown, DMUnLoader draws a backdrop behind its card. The default, .variableBlur, is the backdrop of every release so far: the variable blur of DMVariableBlurView under a black dim. That blur runs a private filter of the system, variableBlur of the CAFilter class, which the README of DMVariableBlurView names; read that README before you ship it. Where the system does not offer that filter, DMVariableBlurView draws the plain blur of the system over the whole screen instead and logs the reason under its own subsystem, unless you set a handler, see below. .dim(), .material() and .clear draw with public API only. Under the system's Reduce Transparency the HUD draws neither the blur nor a material: .variableBlur keeps its dim, .material() gives way to that dim, and the card of the HUD is opaque.

Choosing another backdrop only stops DMUnLoader from creating the variable blur while your app runs. DMUnLoader still depends on DMVariableBlurView, so its code, with the private names a scan of your binary finds, stays in your app. No setting of DMUnLoader 1.1.0 removes it.

To learn that the blur could not be drawn, give the settings a handler: the argument onBackdropFailure of DMLoadingManagerDefaultSettings, or the property of that name of settings of your own.

import DMUnLoader
import OSLog

@MainActor
func makeLoadingManager() -> DMLoadingManagerMain {
    DMLoadingManagerMain(
        state: .none,
        settings: DMLoadingManagerDefaultSettings(
            onBackdropFailure: { failure in
                Logger(subsystem: "com.example.app", category: "hud")
                    .error("The backdrop of the HUD lost its blur: \(String(describing: failure), privacy: .public)")
            }
        )
    )
}

The handler runs on the main actor, with a DMHUDBackdropFailure: effectUnavailable or maskCreationFailed; invalidConfiguration stands for a value of the blur that was not valid, which the HUD, with its own values, does not cause. It is told for each failure that DMVariableBlurView reports. The HUD makes a new blur view each time it appears, so a device that cannot draw the blur reports once for each appearance of a HUD, and DMVariableBlurView can report again when the system rebuilds the effect, for example on a change between light and dark appearance. The handler is read at each render of the HUD, and one that you set later is used for the failures that follow: a failure that happened while the settings had none is not reported afterwards, it went to the log. The handler replaces the line that DMVariableBlurView writes to the log, so write one yourself if you want it. The HUD does not change because of a failure: its state, its buttons and the way it leaves the screen stay as they are. The handler is not called for another backdrop, nor under Reduce Transparency, where no blur is drawn. Settings that do not set one behave as before.

The settings live as long as the loading manager, and so does the handler. A handler that holds the manager, or an object that holds it, strongly makes a reference cycle: capture such an object weakly.

Custom views

Conform a class to DMLoadingViewProvider to replace the views of the states. Every requirement has a default, the library's view, so implement only what you change:

import DMUnLoader
import SwiftUI

final class BrandedViewProvider: DMLoadingViewProvider {
    @MainActor
    func getLoadingView() -> some View {
        ProgressView("Please wait")
            .padding()
            .background(Color.white)
    }

    @MainActor
    func getErrorView(error: any Error, onRetry: (any DMAction)?, onClose: any DMAction) -> some View {
        VStack {
            Text(error.localizedDescription)
            if let onRetry {
                Button("Try again", action: onRetry.simpleAction)
            }
            Button("Close", action: onClose.simpleAction)
        }
        .padding()
        .background(Color.white)
    }
}

View settings

A provider can keep the library's views and change their settings: texts, colours, images and layout. This one only changes the colour of the success image:

import DMUnLoader
import SwiftUI

final class MintSuccessProvider: DMLoadingViewProvider {
    var successViewSettings: any DMSuccessViewSettings {
        DMSuccessDefaultViewSettings(
            successImageProperties: SuccessImageProperties(foregroundColor: .mint)
        )
    }
}

loadingViewSettings, errorViewSettings and successViewSettings take DMProgressViewDefaultSettings, DMErrorDefaultViewSettings and DMSuccessDefaultViewSettings, or a type of your own that conforms to their protocol.

Texts and languages

The default texts of the HUD, the failure title "An error has occurred!", "Close", "Retry" and "Loading...", come from the string catalog of DMUnLoader and follow the language of your app. In 1.1.0 the catalog holds English only, so every language shows these English texts. A text you set in the settings is shown as you wrote it. A text equal to an English default counts as that default, also when you pass it yourself, and follows the catalog.

Behaviour your app must know

Touches

While a HUD is shown, its window takes every touch that reaches it: the windows of your app under it receive none, whatever the backdrop. A tap on the card or outside it hides a success or a failure as the dismissal rules say. Close hides a failure, and Retry runs your action. With no HUD shown, every touch reaches your app.

Threads and lifetime

The loading managers, the view providers and the root view work on the main actor: call them there. Keep the manager alive: in a @StateObject of your app, or the one that the scene delegate creates. The HUD keeps no replaced manager alive. Use one integration per scene: a scene whose scene delegate of DMUnLoader holds a manager and that also shows DMRootLoadingView(manager:content:) gets two HUD windows.

Failures

DMRootLoadingView(content:) without the app delegate of DMUnLoader stops the app when the view appears. DMRootLoadingView(manager:content:) needs no app delegate: until the view is in a window of a scene, the manager waits, and the HUD appears once the view is in one.

Accessibility

While a HUD is shown, DMUnLoader hides from assistive technology, and so from your own UI tests, the windows of its scene at the HUD window's level or below that are open when the HUD appears, until the HUD goes. When the HUD appears or shows another state, VoiceOver is told that the screen changed and starts at its first element: the HUD's, unless a window above the HUD has elements. When the HUD goes, VoiceOver returns to the element it was on, if that element is still on the screen. The escape gesture hides a success or a failure as a tap outside the card does. The loading HUD is one accessibility element named by its text, or by the default loading text ("Loading..." in English) when the text is empty or only whitespace. Reduce Motion keeps the card and a pressed button still. VoiceOver does not read the default images; an image you supply keeps the accessibility you give it.

A success or a failure hides after 2 seconds by default, which can be too short to reach Retry with VoiceOver or Switch Control. An app whose users need more time keeps a failure on the screen with DMHUDDismissal(autoHide: .never), or gives it a longer delay; see The loading manager's settings.

The accessible appearance

The default look of the HUD, white text on a gray card, does not reach a contrast of 4.5:1, and its loading view cuts a long text at large accessibility text sizes. It stays the default so that no screen of your app changes with an update. DMHUDAppearance.accessible is an opt-in look of the library's own views: pass it to the provider that you show a state with.

import DMUnLoader

@MainActor
func showAccessibleLoading(on manager: some DMLoadingManager) {
    manager.showLoading(provider: DefaultDMLoadingViewProvider(appearance: .accessible))
}

It guarantees, and the tests of the package measure:

  • every text at a contrast of 4.5:1 or more against its card, in light and in dark mode;
  • the colour of every icon, progress indicator and button outline at 3:1 or more against the card, and that of the failure sign and the success mark at 4.5:1;
  • an opaque card in the secondary background colour of the system, which follows light and dark mode;
  • a loading text that wraps at the width of the screen and shows in full at every text size, and a card taller than the screen that scrolls;
  • Close and Retry one above the other at accessibility text sizes.

DMHUDAppearance.standard, the look of every release so far, is what every provider gives unless it says otherwise. A provider of your own opts in by returning .accessible from hudAppearance. If it leaves the views to the library, they take the accessible colours too; views of your own get the accessible card and keep the colours they have, so keeping them legible on it is yours to do. A card that grows past the screen, or shrinks back, switches between a plain card and a scrolling one, which builds the views inside it again.

Example app and tests

Examples/DMUnLoaderExample is an app that shows each state. Open DMUnLoaderExample.xcodeproj and run one of its schemes: DMUnLoaderExample (SwiftUI with the app delegate), DMUnLoaderExample-Injected (SwiftUI with a manager the app owns), DMUnLoaderExample-UIKit, or DMUnLoaderExample-CustomManager (a loading manager written by the host). The example of 1.0.x, which resolves the package through both CocoaPods and Swift Package Manager as described in Using Swift Package Manager and CocoaPods with the same SDK, remains at the tag 1.0.3.

How the package is tested:

  • Package tests: the state and the policies of the HUD, the managers and their timers, the adapters with hand-written spies, the views, and snapshots of the views.
  • Tests hosted in the example app: the HUD windows of real scenes, two scenes on an iPad included, the touches they take and let through, and what the HUD draws under Reduce Transparency.
  • UI tests: Retry, Close and taps on and outside the card, the touches that reach the app with and without a HUD in the four integrations, the texts in other languages and right to left, the failure HUD over content compared with reference pictures, and accessibility audits of the HUD: the audit of the standard look leaves out contrast and, for the loading HUD, clipped text and the readability of its "Loading..." label; the audit of the accessible look runs every audit type, at the default text size and at the largest one, and leaves out only that label.
  • CI also lints, compares the public interface with a committed baseline, builds a consumer package and every Swift block of this README and of the documentation catalog, builds the documentation, lints the podspec, runs the tests under the Thread Sanitizer, and fails below a coverage floor.

Versions and migration

DMUnLoader follows semantic versioning. CHANGELOG.md records every release. From the release after 1.1.0 on, every breaking change, as ENGINEERING.md defines it, needs a major version.

Coming from 1.0.x: no declaration was removed, and the new settings default to the released behaviour. Building needs Xcode 26, the Swift 6.2 compiler that the sources already needed. Some behaviour changed, and the changelog lists each change. The ones most likely to matter:

  • Retry, Close and a tap outside the card work on iOS 18 and 26, where the buttons of the failure HUD did nothing;
  • settings and states compare what they show, and a provider and a manager are equal only to themselves;
  • a loading text or a success message of more than one line is centred by default;
  • the default failure title reads "An error has occurred!";
  • the HUD hides the content under it from assistive technology, as described above.

Known issues

The default card of the HUD, white text on gray at opacity 0.8, does not reach the contrast of 4.5:1; an app that needs it passes DMHUDAppearance.accessible to its provider, see The accessible appearance, or sets its own colours through the settings types, or supplies its own views through DMLoadingViewProvider.

At large accessibility text sizes the default loading view, at most 150 points wide, cuts its text; an app that needs those sizes passes DMHUDAppearance.accessible as well, or supplies its own loading view. Changing the default look is a decision for the next major version.

A success or a failure hides after 2 seconds by default. With VoiceOver or Switch Control that can be too short to reach Retry; the Accessibility section names the settings that give more time.

The family

DMUnLoader is one of three packages that share their conventions:

  • DMAction: composes completion-based actions with retries and fallbacks. The Retry button of DMUnLoader runs one.
  • DMVariableBlurView: a blur whose radius changes from row to row. DMUnLoader draws it behind its HUD.

Contributing, security, licence

FOSSA Status

Codecov Snapshot
Code coverage snapshot by file and module (Codecov tree graph)

Visitors

About

Loading, error, and success state presentation for SwiftUI and UIKit.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages