---
title: creaturesurvive/downpour
framework: Swift Package Catalog
role: article
path: packages/creaturesurvive/downpour
---

# creaturesurvive/downpour

A persistent, resumable background download manager for iOS, macOS, tvOS, watchOS and visionOS.

## What it handles for you

| Problem | Downpour | | --- | --- | | App is suspended or killed mid-download | Background session; the queue is persisted as JSON and reattached to live tasks on relaunch | | App was force-quit (the system cancels its tasks) | Lost tasks are re-queued, resuming from stored resume data when possible | | User pauses | `cancel(byProducingResumeData:)`, stored on disk; resume sends a `Range` request | | Wi-Fi drops mid-transfer | Resume data is pulled from the error and retried automatically with exponential backoff | | Server returns 404, 500 or 503 | URLSession reports *success* with the error page as your "file". Downpour checks the status, never moves the error body into place, and retries only 408, 429 and 5xx | | Stale resume data | Discarded, and the download restarts cleanly | | Temporary file deleted before you move it | The move happens synchronously inside `didFinishDownloadingTo`; directories are created and existing files replaced | | Too many simultaneous transfers | Configurable concurrency limit and per-download priority | | Disk full | Checks free space against the expected size before starting | | iCloud backup bloat | Finished files are excluded from backup (configurable) | | `handleEventsForBackgroundURLSession` | Called for you once all events are delivered | | Events for tasks from a previous launch arriving before restoration | Tasks are adopted by identifier instead of being dropped |

## Installation

Add Downpour to your `Package.swift`:

```swift dependencies: [     .package(url: "https://github.com/CreatureSurvive/Downpour.git", from: "1.0.0"), ], targets: [     .target(name: "MyApp", dependencies: ["Downpour"]), ] ```

Or in Xcode, choose **File › Add Package Dependencies…** and enter `https://github.com/CreatureSurvive/Downpour`.

### Requirements

| Platform | Minimum | | --- | --- | | iOS | 17.0 | | macOS | 14.0 | | tvOS | 17.0 | | watchOS | 10.0 | | visionOS | 1.0 |

Swift 6.1 (Xcode 16.4) or later, in Swift 6 language mode. No third-party dependencies.

## Setup

Create **one** manager per session identifier at launch and keep it for the lifetime of the app:

```swift extension DownloadManager {     static let shared = DownloadManager(configuration: {         var configuration = DownloadManager.Configuration(sessionIdentifier: "com.example.downloads")         configuration.maximumConcurrentDownloads = 2         configuration.allowsCellularAccess = false         return configuration     }()) } ```

Forward background events (iOS, tvOS, visionOS):

```swift final class AppDelegate: NSObject, UIApplicationDelegate {     func application(_ application: UIApplication,                      handleEventsForBackgroundURLSession identifier: String,                      completionHandler: @escaping () -> Void) {         DownloadManager.shared.handleEventsForBackgroundURLSession(identifier: identifier, completionHandler: completionHandler)     } } ```

## Usage

```swift let manager = DownloadManager.shared

try manager.enqueue(request)                         // throws .duplicate if the id exists try manager.enqueue(request, replacingExisting: true)

manager.pause(id) manager.resume(id) manager.retry(id) manager.pauseAll() manager.resumeAll() manager.setPriority(10, for: id) manager.remove(id, deleteFile: true)

manager.downloads                                    // [Download], in enqueue order manager.download(withID: id)?.progress               // 0...1 manager.hasResumeData(for: id)

for await event in manager.eventStream() {           // .added, .progress, .statusChanged, .removed     ... } ```

`DownloadRequest.userInfo` persists arbitrary strings with the download, such as a title or a JSON-encoded model. That lets you rebuild your UI after relaunch without keeping a separate database.

### SwiftUI

```swift struct DownloadsView: View {     @State private var store = DownloadStore(manager: .shared)

var body: some View {         List(store.downloads) { download in             VStack(alignment: .leading) {                 Text(download.request.userInfo["title"] ?? download.id)                 switch download.status {                 case .downloading, .queued, .waitingToRetry:                     ProgressView(value: download.progress ?? 0)                 case .paused: Text("Paused")                 case .failed: Text(download.error?.localizedDescription ?? "Failed")                 case .completed: Text("Downloaded")                 }             }         }     } } ```

`DownloadStore` is `@Observable`. Progress events update only the affected entry.

## Download lifecycle

``` queued ──▶ downloading ──▶ completed   ▲  │          │  │   │  ▼          │  └──▶ waitingToRetry ──(backoff)──▶ queued   paused ◀──────┘              │                                └──(attempts exhausted / permanent error)──▶ failed ──(retry/resume)──▶ queued ```

## Testing

`swift test` drives real `URLSession` transfers against an in-process HTTP server that supports byte ranges, throttled streaming, flaky 503s and connections that drop mid-body. The tests cover completion, HTTP error handling, retry and backoff, resume after a dropped connection (verified to use `Range`), pause and resume, concurrency limits, priorities, relaunch restoration, duplicates, removal and storage checks. The suite is clean under Thread Sanitizer.

Tests use a foreground session (`sessionIdentifier: nil`) because background sessions need an app bundle. The same code paths are used for both.

## Changelog

See [CHANGELOG.md](CHANGELOG.md). Releases follow [Semantic Versioning](https://semver.org).

## Contributing

Issues and pull requests are welcome. Please run `swift test` before opening a pull request, and add tests for new behavior.

## License

Available under the MIT license. See [LICENSE](LICENSE) for details.

## Package Metadata

Repository: creaturesurvive/downpour

Default branch: main

README: README.md
