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 26.0
run: sudo xcode-select -s /Applications/Xcode_26.app/Contents/Developer
- name: Select Xcode 26.0.1
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer
- name: Build
run: swift build -v
- name: Run tests
Expand Down
196 changes: 92 additions & 104 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ A Swift package that provides elegant state management for asynchronous operatio

AsyncLoad provides components for handling asynchronous operations:
- `AsyncLoad<T>`: For loading data 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

Expand Down Expand Up @@ -44,14 +42,16 @@ Or add it through Xcode:
An enum that represents the state of an asynchronous data loading operation.

```swift
public enum AsyncLoad<T>: Equatable {
public enum AsyncLoad<T: Equatable & Sendable>: Equatable, Sendable {
case none // Initial state
case loading // Loading in progress
case error(Error)// Loading failed with error
case loaded(T) // Successfully loaded with data
}
```

> **Note**: The generic type `T` must conform to both `Equatable` and `Sendable` protocols.

#### Properties

- `isLoading: Bool` - Returns true if the state is `.loading`
Expand Down Expand Up @@ -80,64 +80,25 @@ class DataViewModel {
}
```

### AsyncAction<T>

Similar to AsyncLoad but designed for action-based operations (like posting data, submitting forms, etc.).

```swift
public enum AsyncAction<T>: Equatable {
case none // Initial state
case loading // Action in progress
case error(Error) // Action failed with error
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 FormViewModel {
var submitAction: AsyncAction<SubmitResponse> = .none

func submitForm(data: FormData) async {
submitAction = .loading

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

### CachedAsyncLoad<T>

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

```swift
public enum CachedAsyncLoad<T>: Equatable {
public enum CachedAsyncLoad<T: Equatable & Sendable>: Equatable, Sendable {
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
}
```

> **Note**: The generic type `T` must conform to both `Equatable` and `Sendable` protocols.

#### Properties

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

#### Example Usage
Expand Down Expand Up @@ -169,53 +130,6 @@ class CachedDataViewModel {
}
```

### 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 All @@ -236,7 +150,7 @@ public init(

// With default error content (Text)
public init(
_ state: AsyncLoad<Item>,
_ state: AsyncLoad<Item>,
@ViewBuilder content: @escaping (Item?) -> Content
) where ErrorContent == Text
```
Expand All @@ -249,7 +163,7 @@ import AsyncLoad

struct UserProfileView: View {
@State private var viewModel = UserProfileViewModel()

var body: some View {
AsyncLoadView(viewModel.userProfile) { user in
if let user = user {
Expand Down Expand Up @@ -277,22 +191,96 @@ struct UserProfileView: View {
}
```

### CachedAsyncLoadView

A SwiftUI view component that handles cached async states with separate loading content.

```swift
public struct CachedAsyncLoadView<Item, Content: View, ErrorContent: View, LoadingContent: View>: View
```

#### Initializer

```swift
public init(
_ state: CachedAsyncLoad<Item>,
@ViewBuilder content: @escaping (Item) -> Content,
@ViewBuilder loading: @escaping (Item?) -> LoadingContent,
@ViewBuilder error: @escaping (Item?, Error) -> ErrorContent
)
```

#### Example Usage

```swift
import SwiftUI
import AsyncLoad

struct CachedUserProfileView: View {
@State private var viewModel = CachedUserProfileViewModel()

var body: some View {
CachedAsyncLoadView(viewModel.userProfile) { user in
// Content view - only called when data is loaded
VStack(alignment: .leading) {
Text(user.name)
.font(.title)
Text(user.email)
.foregroundStyle(.secondary)
}
} loading: { cachedUser in
// Loading view - receives cached data if available
VStack {
if let cachedUser {
VStack(alignment: .leading) {
Text(cachedUser.name)
.font(.title)
Text(cachedUser.email)
.foregroundStyle(.secondary)
}
.opacity(0.5)
}
ProgressView()
}
} error: { cachedUser, error in
// Error view - receives cached data if available
VStack {
if let cachedUser {
VStack(alignment: .leading) {
Text(cachedUser.name)
.font(.title)
Text(cachedUser.email)
.foregroundStyle(.secondary)
}
.opacity(0.5)
}
Text("Error: \(error.localizedDescription)")
.foregroundColor(.red)
}
}
.task {
await viewModel.loadUserProfile(id: "123")
}
}
}
```

## Features

- **Type-safe**: Generic enums ensure type safety for your data
- **Equatable**: All async state enums conform to Equatable for easy state comparison
- **Equatable**: All async state enums and their generic types conform to Equatable for easy state comparison
- **Sendable**: Full Swift 6 concurrency support with Sendable conformance for thread-safe async operations
- **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
- **Cached Data**: CachedAsyncLoad preserves 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. **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)
1. **Use AsyncLoad for simple loading operations** where you don't need to preserve data during refreshes
2. **Use CachedAsyncLoad** when you want to preserve data during refreshes or show stale data during errors
3. **Ensure your data types conform to Equatable and Sendable** - All generic types used with AsyncLoad components must implement both protocols
4. **Always handle all states** in your UI to provide good user experience
5. **Use AsyncLoadView and CachedAsyncLoadView** for simple cases to reduce boilerplate code
6. **Reset states** appropriately (e.g., set to `.none` when appropriate)
78 changes: 0 additions & 78 deletions Sources/AsyncLoad/AsyncLoad/AsyncAction.swift

This file was deleted.

Loading