SwiftUI Loading Indicator

Often in an app, you need to show a loading indicator that covers the screen and prevents the user from tapping anything during a network request. Here’s a simple way to do so that covers everything, including such things as UINavigationController bars.

You can use it from anywhere, no View/UIViewController/anything context required, so it’s easy to use from eg a ViewModel like so:

Task {
    var loading: LoadingHandle? = showLoading()
    defer { loading?.dummy(); loading = nil }

    // ... do network calls here ...
    try await Task.sleep(nanoseconds: NSEC_PER_SEC)
}

The call to dummy prevents the compiler from complaining because it can’t figure out what the loading variable is being used for.

When the loading handle is released, it automatically removes itself from the screen.

The ‘defer’ block ensures that this is released at the end of the task, regardless of whether an error is thrown or not. Using defer makes this explicit: If you didn’t use defer and simply relied on the compiler’s auto-reference-counting to release it, it might release it at the end, or maybe at a suspension point, or maybe immediately, it’s hard to be sure, so best to use defer to keep it explicit.

And here’s the loader code:

/// This is for showing a loading overlay that prevents the user from tapping eg nav bars.
/// Store the returned handle in an optional var and defer nilling said var.
/// The handle will automatically dismiss the loading when released.
func showLoading() -> LoadingHandle? {
    guard let window = UIApplication.shared.keyWindow else { return nil }
    let view = LoadingOverlayView()
    let host = UIHostingController(rootView: view)
    host.view.backgroundColor = .clear
    host.view.frame = window.bounds
    window.addSubview(host.view)
    return LoadingHandle(vc: host)
}

// This is responsible for removing the loader at the end of this's lifespan.
class LoadingHandle {
    private let vc: UIViewController
    init(vc: UIViewController) {
        self.vc = vc
    }
    func dummy() {} // To remove a compiler warning.
    deinit {
        vc.view.removeFromSuperview()
    }
}

// Helper to find windows:
private extension UApplication {
    var keyWindow: UIWindow? {
        let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
        let windows = scenes.flatMap { $0.windows }
        let key = windows.first(where: \.isKeyWindow)
        return key
    }
}

// Full-screen loading overlay:
struct LoadingOverlayView: View {
    var body: some View {
        ZStack {
            Color.primary.opacity(0.5)
                .ignoresSafeArea()
            ProgressView()
                .tint(Color(uiColor: .systemBackground))
        }
    }
}

Thanks for reading, I pinky promise this was written by a human, not AI, hope you found this helpful, at least a tiny bit, God bless!

Photo from unsplash.com/@joshfrenette

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

Software Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, CommBank, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS