Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/swift-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ jobs:

steps:
- uses: actions/checkout@v4
- name: Select Xcode 16.4
run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer
- name: Select Xcode 26.0
run: sudo xcode-select -s /Applications/Xcode_26.app/Contents/Developer
- name: Build
run: swift build -v
- name: Run tests
Expand Down
117 changes: 109 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ A Swift package that provides elegant state management for asynchronous operatio

## Overview

AsyncLoad provides two main components for handling asynchronous operations:
AsyncLoad provides components for handling asynchronous operations:
- `AsyncLoad<T>`: For loading data operations
- `AsyncAction<T>`: For action-based operations
- `AsyncAction<T>`: For action-based operations
- `CachedAsyncLoad<T>`: For loading operations that preserve cached data during refreshes
- `CachedAsyncAction<T>`: For actions that preserve cached data during retries
- `AsyncLoadView`: A SwiftUI view component for displaying async states
- `CachedAsyncLoadView`: A SwiftUI view component for cached async states

## Requirements

Expand All @@ -25,7 +28,7 @@ Add AsyncLoad to your project by adding the following to your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/yourusername/AsyncLoad", from: "1.0.0")
.package(url: "https://github.com/diamirio/AsyncLoad", from: "1.0.0")
]
```

Expand Down Expand Up @@ -118,6 +121,101 @@ class FormViewModel {
}
```

### CachedAsyncLoad<T>

An enhanced version of AsyncLoad that preserves cached data during loading and error states.

```swift
public enum CachedAsyncLoad<T>: Equatable {
case none // Initial state
case loading(T? = nil) // Loading with optional cached data
case error(T? = nil, Error) // Error with optional cached data
case loaded(T) // Successfully loaded with data
}
```

#### Properties

- `isLoading: Bool` - Returns true if the state is `.loading`
- `item: T?` - Returns the loaded item if state is `.loaded`, nil otherwise
- `error: Error?` - Returns the error if state is `.error`, nil otherwise

#### Example Usage

```swift
import AsyncLoad

@Observable
class CachedDataViewModel {
var userProfile: CachedAsyncLoad<User> = .none

func loadUserProfile(id: String) async {
// Start loading while preserving any existing data
if case .loaded(let existingUser) = userProfile {
userProfile = .loading(existingUser)
} else {
userProfile = .loading()
}

do {
let user = try await userService.fetchUser(id: id)
userProfile = .loaded(user)
} catch {
// Preserve existing data even during error
let existingUser = userProfile.item
userProfile = .error(existingUser, error)
}
}
}
```

### CachedAsyncAction<T>

Similar to AsyncAction but preserves cached data during loading and error states.

```swift
public enum CachedAsyncAction<T>: Equatable {
case none // Initial state
case loading(T? = nil) // Action in progress with optional cached data
case error(T? = nil, Error) // Action failed with optional cached data
case success(T) // Action completed successfully
}
```

#### Properties

- `isLoading: Bool` - Returns true if the state is `.loading`
- `item: T?` - Returns the success result if state is `.success`, nil otherwise
- `error: Error?` - Returns the error if state is `.error`, nil otherwise

#### Example Usage

```swift
import AsyncLoad

@Observable
class CachedFormViewModel {
var submitAction: CachedAsyncAction<SubmitResponse> = .none

func submitForm(data: FormData) async {
// Preserve previous successful response during retry
if case .success(let previousResponse) = submitAction {
submitAction = .loading(previousResponse)
} else {
submitAction = .loading()
}

do {
let response = try await apiService.submit(data)
submitAction = .success(response)
} catch {
let previousResponse = submitAction.item
submitAction = .error(previousResponse, error)
}
}
}
```

### AsyncLoadView

A SwiftUI view component that automatically handles the display of different async states.
Expand Down Expand Up @@ -182,16 +280,19 @@ struct UserProfileView: View {
## Features

- **Type-safe**: Generic enums ensure type safety for your data
- **Equatable**: Both AsyncLoad and AsyncAction conform to Equatable for easy state comparison
- **SwiftUI Integration**: AsyncLoadView provides seamless integration with SwiftUI
- **Equatable**: All async state enums conform to Equatable for easy state comparison
- **SwiftUI Integration**: AsyncLoadView and CachedAsyncLoadView provide seamless integration with SwiftUI
- **Error Handling**: Built-in error state management
- **Loading States**: Automatic loading state handling with progress indicators
- **Cached Data**: CachedAsyncLoad and CachedAsyncAction preserve data during refreshes and errors
- **Flexible UI**: Customizable content and error views

## Best Practices

1. **Use AsyncLoad for data fetching** operations (GET requests, loading content)
2. **Use AsyncAction for user actions** (POST/PUT/DELETE requests, form submissions)
3. **Always handle all states** in your UI to provide good user experience
4. **Use AsyncLoadView** for simple cases to reduce boilerplate code
5. **Reset states** appropriately (e.g., set to `.none` when appropriate)
3. **Use CachedAsyncLoad** when you want to preserve data during refreshes or show stale data during errors
4. **Use CachedAsyncAction** when you want to preserve previous results during action retries
5. **Always handle all states** in your UI to provide good user experience
6. **Use AsyncLoadView and CachedAsyncLoadView** for simple cases to reduce boilerplate code
7. **Reset states** appropriately (e.g., set to `.none` when appropriate)
32 changes: 31 additions & 1 deletion Sources/AsyncLoad/AsyncLoad/AsyncAction.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public enum AsyncAction<T>: Equatable {
public enum AsyncAction<T: Sendable>: Equatable, Sendable {
case none
case loading
case error(Error)
Expand Down Expand Up @@ -45,4 +45,34 @@ public enum AsyncAction<T>: Equatable {
false
}
}

public static func == (lhs: AsyncAction<T>, rhs: AsyncAction<T>) -> Bool where T : Equatable {
switch (lhs, rhs) {
case (.none, .none):
true
case (.loading, .loading):
true
case (.error, .error):
true
case let (.success(lhsItem), .success(rhsItem)):
lhsItem == rhsItem
default:
false
}
}

public static func != (lhs: AsyncAction<T>, rhs: AsyncAction<T>) -> Bool where T : Equatable {
switch (lhs, rhs) {
case (.none, .none):
false
case (.loading, .loading):
false
case (.error, .error):
false
case let (.success(lhsItem), .success(rhsItem)):
lhsItem != rhsItem
default:
true
}
}
}
32 changes: 31 additions & 1 deletion Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public enum AsyncLoad<T>: Equatable {
public enum AsyncLoad<T: Sendable>: Equatable, Sendable {
case none
case loading
case error(Error)
Expand Down Expand Up @@ -45,4 +45,34 @@ public enum AsyncLoad<T>: Equatable {
false
}
}

public static func == (lhs: AsyncLoad<T>, rhs: AsyncLoad<T>) -> Bool where T : Equatable {
switch (lhs, rhs) {
case (.none, .none):
true
case (.loading, .loading):
true
case (.error, .error):
true
case let (.loaded(lhsItem), .loaded(rhsItem)):
lhsItem == rhsItem
default:
false
}
}

public static func != (lhs: AsyncLoad<T>, rhs: AsyncLoad<T>) -> Bool where T : Equatable {
switch (lhs, rhs) {
case (.none, .none):
false
case (.loading, .loading):
false
case (.error, .error):
false
case let (.loaded(rhsItem), .loaded(lhsItem)):
lhsItem != rhsItem
default:
true
}
}
}
6 changes: 5 additions & 1 deletion Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import Foundation
#if canImport(SwiftUI)
import SwiftUI

public struct AsyncLoadView<Item, Content: View, ErrorContent: View>: View {
public struct AsyncLoadView<
Item: Sendable,
Content: View,
ErrorContent: View
>: View {
let state: AsyncLoad<Item>

@ViewBuilder
Expand Down
32 changes: 31 additions & 1 deletion Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public enum CachedAsyncAction<T>: Equatable {
public enum CachedAsyncAction<T: Sendable>: Equatable, Sendable {
case none
case loading(T? = nil)
case error(T? = nil, Error)
Expand Down Expand Up @@ -45,4 +45,34 @@ public enum CachedAsyncAction<T>: Equatable {
false
}
}

public static func == (lhs: CachedAsyncAction<T>, rhs: CachedAsyncAction<T>) -> Bool where T : Equatable {
switch (lhs, rhs) {
case (.none, .none):
true
case let (.loading(lhsItem), .loading(rhsItem)):
lhsItem == rhsItem
case let (.error(lhsItem, _), .error(rhsItem, _)):
lhsItem == rhsItem
case let (.success(lhsItem), .success(rhsItem)):
lhsItem == rhsItem
default:
false
}
}

public static func != (lhs: CachedAsyncAction<T>, rhs: CachedAsyncAction<T>) -> Bool where T : Equatable {
switch (lhs, rhs) {
case (.none, .none):
false
case let (.loading(lhsItem), .loading(rhsItem)):
lhsItem != rhsItem
case let (.error(lhsItem, _), .error(rhsItem, _)):
lhsItem != rhsItem
case let (.success(lhsItem), .success(rhsItem)):
lhsItem != rhsItem
default:
false
}
}
}
32 changes: 31 additions & 1 deletion Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public enum CachedAsyncLoad<T>: Equatable {
public enum CachedAsyncLoad<T: Sendable>: Equatable, Sendable {
case none
case loading(T? = nil)
case error(T? = nil, Error)
Expand Down Expand Up @@ -45,4 +45,34 @@ public enum CachedAsyncLoad<T>: Equatable {
false
}
}

public static func == (lhs: CachedAsyncLoad<T>, rhs: CachedAsyncLoad<T>) -> Bool where T : Equatable {
switch (lhs, rhs) {
case (.none, .none):
true
case let (.loading(lhsItem), .loading(rhsItem)):
lhsItem == rhsItem
case let (.error(lhsItem, _), .error(rhsItem, _)):
lhsItem == rhsItem
case let (.loaded( lhsItem), .loaded(rhsItem)):
lhsItem == rhsItem
default:
false
}
}

public static func != (lhs: CachedAsyncLoad<T>, rhs: CachedAsyncLoad<T>) -> Bool where T : Equatable {
switch (lhs, rhs) {
case (.none, .none):
true
case let (.loading(lhsItem), .loading(rhsItem)):
lhsItem != rhsItem
case let (.error(lhsItem, _), .error(rhsItem, _)):
lhsItem != rhsItem
case let (.loaded( lhsItem), .loaded(rhsItem)):
lhsItem != rhsItem
default:
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
import SwiftUI

public struct CachedAsyncLoadView<
Item,
Item: Sendable,
Content: View,
ErrorContent: View,
LoadingContent: View
Expand Down
Loading