From 36debdd3c29af5ec5fcba965bf9520a816044a5b Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 30 Sep 2025 12:34:29 +0200 Subject: [PATCH 1/6] Added CachedAsyncLoadView and CachedAsyncLoad documentation to README --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ab40b97..1840393 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ public enum CachedAsyncLoad: Equatable { #### 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 @@ -236,7 +236,7 @@ public init( // With default error content (Text) public init( - _ state: AsyncLoad, + _ state: AsyncLoad, @ViewBuilder content: @escaping (Item?) -> Content ) where ErrorContent == Text ``` @@ -249,7 +249,7 @@ import AsyncLoad struct UserProfileView: View { @State private var viewModel = UserProfileViewModel() - + var body: some View { AsyncLoadView(viewModel.userProfile) { user in if let user = user { @@ -277,6 +277,80 @@ struct UserProfileView: View { } ``` +### CachedAsyncLoadView + +A SwiftUI view component that handles cached async states with separate loading content. + +```swift +public struct CachedAsyncLoadView: View +``` + +#### Initializer + +```swift +public init( + _ state: CachedAsyncLoad, + @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 From 5a7b08c57a4b3c64fbb9de09e399086898611f3a Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 30 Sep 2025 12:48:46 +0200 Subject: [PATCH 2/6] Added further README content --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1840393..46d55fd 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ public enum CachedAsyncAction: Equatable { #### Properties - `isLoading: Bool` - Returns true if the state is `.loading` -- `item: T?` - Returns the success result if state is `.success`, nil otherwise +- `item: T?` - Returns the item from `.success`, `.loading`, or `.error` states, nil for `.none` - `error: Error?` - Returns the error if state is `.error`, nil otherwise #### Example Usage From 600c2ded95d73d6de6794dedc2d7e2976d1c6ca2 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 13 Oct 2025 13:46:44 +0200 Subject: [PATCH 3/6] Fixed CachedAsyncLoad and AsyncLoad to only allow Equatable content --- Sources/AsyncLoad/AsyncLoad/AsyncAction.swift | 32 +------------------ Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift | 32 +------------------ .../AsyncLoad/AsyncLoad/AsyncLoadView.swift | 2 +- .../CachedAsyncLoad/CachedAsyncAction.swift | 32 +------------------ .../CachedAsyncLoad/CachedAsyncLoad.swift | 32 +------------------ .../CachedAsyncLoad/CachedAsyncLoadView.swift | 6 ++-- .../AsyncActionNonEquatableItemTests.swift | 26 --------------- .../AsyncLoadNonEquatableItemTests.swift | 26 --------------- ...chedAsyncActionNonEquatableItemTests.swift | 19 ----------- ...CachedAsyncLoadNonEquatableItemTests.swift | 19 ----------- Tests/AsyncLoadTests/Utils.swift | 8 ++--- 11 files changed, 13 insertions(+), 221 deletions(-) delete mode 100644 Tests/AsyncLoadTests/AsyncActionNonEquatableItemTests.swift delete mode 100644 Tests/AsyncLoadTests/AsyncLoadNonEquatableItemTests.swift delete mode 100644 Tests/AsyncLoadTests/CachedAsyncActionNonEquatableItemTests.swift delete mode 100644 Tests/AsyncLoadTests/CachedAsyncLoadNonEquatableItemTests.swift diff --git a/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift b/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift index 215b3d8..e68822d 100644 --- a/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift +++ b/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift @@ -1,6 +1,6 @@ import Foundation -public enum AsyncAction: Equatable, Sendable { +public enum AsyncAction: Equatable, Sendable { case none case loading case error(Error) @@ -32,21 +32,6 @@ public enum AsyncAction: Equatable, Sendable { } public static func == (lhs: AsyncAction, rhs: AsyncAction) -> Bool { - switch (lhs, rhs) { - case (.none, .none): - true - case (.loading, .loading): - true - case (.error, .error): - true - case (.success, .success): - true - default: - false - } - } - - public static func == (lhs: AsyncAction, rhs: AsyncAction) -> Bool where T : Equatable { switch (lhs, rhs) { case (.none, .none): true @@ -60,19 +45,4 @@ public enum AsyncAction: Equatable, Sendable { false } } - - public static func != (lhs: AsyncAction, rhs: AsyncAction) -> 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 - } - } } diff --git a/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift b/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift index 2fb48f9..65b4004 100644 --- a/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift +++ b/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift @@ -1,6 +1,6 @@ import Foundation -public enum AsyncLoad: Equatable, Sendable { +public enum AsyncLoad: Equatable, Sendable { case none case loading case error(Error) @@ -45,34 +45,4 @@ public enum AsyncLoad: Equatable, Sendable { false } } - - public static func == (lhs: AsyncLoad, rhs: AsyncLoad) -> 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, rhs: AsyncLoad) -> 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 - } - } } diff --git a/Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift b/Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift index 709e27b..9d13e70 100644 --- a/Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift +++ b/Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift @@ -3,7 +3,7 @@ import Foundation import SwiftUI public struct AsyncLoadView< - Item: Sendable, + Item: Equatable & Sendable, Content: View, ErrorContent: View >: View { diff --git a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift index 162cc58..4bc8a7e 100644 --- a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift +++ b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift @@ -1,6 +1,6 @@ import Foundation -public enum CachedAsyncAction: Equatable, Sendable { +public enum CachedAsyncAction: Equatable, Sendable { case none case loading(T? = nil) case error(T? = nil, Error) @@ -32,21 +32,6 @@ public enum CachedAsyncAction: Equatable, Sendable { } public static func == (lhs: CachedAsyncAction, rhs: CachedAsyncAction) -> Bool { - switch (lhs, rhs) { - case (.none, .none): - true - case (.loading, .loading): - true - case (.error, .error): - true - case (.success, .success): - true - default: - false - } - } - - public static func == (lhs: CachedAsyncAction, rhs: CachedAsyncAction) -> Bool where T : Equatable { switch (lhs, rhs) { case (.none, .none): true @@ -60,19 +45,4 @@ public enum CachedAsyncAction: Equatable, Sendable { false } } - - public static func != (lhs: CachedAsyncAction, rhs: CachedAsyncAction) -> 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 - } - } } diff --git a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift index 826a1dc..bc1da9b 100644 --- a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift +++ b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift @@ -1,6 +1,6 @@ import Foundation -public enum CachedAsyncLoad: Equatable, Sendable { +public enum CachedAsyncLoad: Equatable, Sendable { case none case loading(T? = nil) case error(T? = nil, Error) @@ -36,21 +36,6 @@ public enum CachedAsyncLoad: Equatable, Sendable { } public static func == (lhs: CachedAsyncLoad, rhs: CachedAsyncLoad) -> Bool { - switch (lhs, rhs) { - case (.none, .none): - true - case (.loading, .loading): - true - case (.error, .error): - true - case (.loaded, .loaded): - true - default: - false - } - } - - public static func == (lhs: CachedAsyncLoad, rhs: CachedAsyncLoad) -> Bool where T : Equatable { switch (lhs, rhs) { case (.none, .none): true @@ -64,19 +49,4 @@ public enum CachedAsyncLoad: Equatable, Sendable { false } } - - public static func != (lhs: CachedAsyncLoad, rhs: CachedAsyncLoad) -> 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 - } - } } diff --git a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift index dd4a180..2eb717f 100644 --- a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift +++ b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift @@ -3,7 +3,7 @@ import Foundation import SwiftUI public struct CachedAsyncLoadView< - Item: Sendable, + Item: Equatable & Sendable, Content: View, ErrorContent: View, LoadingContent: View @@ -47,7 +47,6 @@ public struct CachedAsyncLoadView< } } } -#endif fileprivate enum CustomError: Error { case test @@ -93,6 +92,8 @@ fileprivate enum CustomError: Error { try await Task.sleep(for: .seconds(2)) state = .loaded("Working!") try await Task.sleep(for: .seconds(2)) + state = .loaded("Working1234") + try await Task.sleep(for: .seconds(2)) state = .loading("Working!") try await Task.sleep(for: .seconds(2)) state = .error("Working!", CustomError.test) @@ -100,3 +101,4 @@ fileprivate enum CustomError: Error { } } } +#endif diff --git a/Tests/AsyncLoadTests/AsyncActionNonEquatableItemTests.swift b/Tests/AsyncLoadTests/AsyncActionNonEquatableItemTests.swift deleted file mode 100644 index c5b45dd..0000000 --- a/Tests/AsyncLoadTests/AsyncActionNonEquatableItemTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Testing -@testable import AsyncLoad - -@Suite("Test non-equatable AsyncAction") -struct AsyncActionNonEquatableItemTests { - @Test<[AsyncActionParameter]>("Should be equal (structural)", arguments: [ - .init(.none, .none), - .init(.loading, .loading), - .init(.success(NonEquatableItem(name: "Hello")), .success(NonEquatableItem(name: "Different"))), - .init(.success(NonEquatableItem(name: "Some")), .success(NonEquatableItem(name: "Other"))), - .init(.error(TestingError.some), .error(TestingError.some)), - .init(.error(TestingError.some), .error(TestingError.other)), - ]) - func structuralEquality(param: AsyncActionParameter) async throws { - #expect(param.action1 == param.action2) - } - - @Test<[AsyncActionParameter]>("Should be equal (mixed types)", arguments: [ - .init(.loading, .loading), - .init(.error(TestingError.some), .error(TestingError.some)), - .init(.error(TestingError.some), .error(TestingError.other)), - ]) - func mixedTypeEquality(param: AsyncActionParameter) async throws { - #expect(param.action1 == param.action2) - } -} diff --git a/Tests/AsyncLoadTests/AsyncLoadNonEquatableItemTests.swift b/Tests/AsyncLoadTests/AsyncLoadNonEquatableItemTests.swift deleted file mode 100644 index 9281991..0000000 --- a/Tests/AsyncLoadTests/AsyncLoadNonEquatableItemTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Testing -@testable import AsyncLoad - -@Suite("Test non-equatable AsyncLoad") -struct AsyncLoadNonEquatableItemTests { - @Test<[AsyncLoadParameter]>("Should be equal (structural)", arguments: [ - .init(.none, .none), - .init(.loading, .loading), - .init(.loaded(NonEquatableItem(name: "Hello")), .loaded(NonEquatableItem(name: "Different"))), - .init(.loaded(NonEquatableItem(name: "Some")), .loaded(NonEquatableItem(name: "Other"))), - .init(.error(TestingError.some), .error(TestingError.some)), - .init(.error(TestingError.some), .error(TestingError.other)), - ]) - func structuralEquality(param: AsyncLoadParameter) async throws { - #expect(param.load1 == param.load2) - } - - @Test<[AsyncLoadParameter]>("Should be equal (mixed types)", arguments: [ - .init(.loading, .loading), - .init(.error(TestingError.some), .error(TestingError.some)), - .init(.error(TestingError.some), .error(TestingError.other)), - ]) - func mixedTypeEquality(param: AsyncLoadParameter) async throws { - #expect(param.load1 == param.load2) - } -} diff --git a/Tests/AsyncLoadTests/CachedAsyncActionNonEquatableItemTests.swift b/Tests/AsyncLoadTests/CachedAsyncActionNonEquatableItemTests.swift deleted file mode 100644 index e369b89..0000000 --- a/Tests/AsyncLoadTests/CachedAsyncActionNonEquatableItemTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Testing -@testable import AsyncLoad - -@Suite("Test non-equatable CachedAsyncAction") -struct CachedAsyncActionNonEquatableItemTests { - @Test<[CachedAsyncActionParameter]>("Should be equal (structural)", arguments: [ - .init(.none, .none), - .init(.loading(), .loading()), - .init(.success(NonEquatableItem(name: "Hello")), .success(NonEquatableItem(name: "Different"))), - .init(.success(NonEquatableItem(name: "Some")), .success(NonEquatableItem(name: "Other"))), - .init(.loading(NonEquatableItem(name: "Some")), .loading(NonEquatableItem(name: "Other"))), - .init(.error(nil, TestingError.some), .error(nil, TestingError.some)), - .init(.error(nil, TestingError.some), .error(NonEquatableItem(name: "Hello"), TestingError.other)), - .init(.error(NonEquatableItem(name: "Hello"), TestingError.some), .error(NonEquatableItem(name: "World"), TestingError.some)), - ]) - func structuralEquality(param: CachedAsyncActionParameter) async throws { - #expect(param.action1 == param.action2) - } -} diff --git a/Tests/AsyncLoadTests/CachedAsyncLoadNonEquatableItemTests.swift b/Tests/AsyncLoadTests/CachedAsyncLoadNonEquatableItemTests.swift deleted file mode 100644 index c55af85..0000000 --- a/Tests/AsyncLoadTests/CachedAsyncLoadNonEquatableItemTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Testing -@testable import AsyncLoad - -@Suite("Test non-equatable CachedAsyncLoad") -struct CachedAsyncLoadNonEquatableItemTests { - @Test<[CachedAsyncLoadParameter]>("Should be equal (structural)", arguments: [ - .init(.none, .none), - .init(.loading(), .loading()), - .init(.loaded(NonEquatableItem(name: "Hello")), .loaded(NonEquatableItem(name: "Different"))), - .init(.loaded(NonEquatableItem(name: "Some")), .loaded(NonEquatableItem(name: "Other"))), - .init(.loading(NonEquatableItem(name: "Some")), .loading(NonEquatableItem(name: "Other"))), - .init(.error(nil, TestingError.some), .error(nil, TestingError.some)), - .init(.error(nil, TestingError.some), .error(NonEquatableItem(name: "Hello"), TestingError.other)), - .init(.error(NonEquatableItem(name: "Hello"), TestingError.some), .error(NonEquatableItem(name: "World"), TestingError.some)), - ]) - func structuralEquality(param: CachedAsyncLoadParameter) async throws { - #expect(param.load1 == param.load2) - } -} diff --git a/Tests/AsyncLoadTests/Utils.swift b/Tests/AsyncLoadTests/Utils.swift index b3cdbb0..78d4299 100644 --- a/Tests/AsyncLoadTests/Utils.swift +++ b/Tests/AsyncLoadTests/Utils.swift @@ -18,7 +18,7 @@ struct User: Equatable { let name: String } -struct AsyncActionParameter { +struct AsyncActionParameter { let action1: AsyncAction let action2: AsyncAction @@ -28,7 +28,7 @@ struct AsyncActionParameter { } } -struct AsyncLoadParameter { +struct AsyncLoadParameter { let load1: AsyncLoad let load2: AsyncLoad @@ -38,7 +38,7 @@ struct AsyncLoadParameter { } } -struct CachedAsyncActionParameter { +struct CachedAsyncActionParameter { let action1: CachedAsyncAction let action2: CachedAsyncAction @@ -48,7 +48,7 @@ struct CachedAsyncActionParameter { } } -struct CachedAsyncLoadParameter { +struct CachedAsyncLoadParameter { let load1: CachedAsyncLoad let load2: CachedAsyncLoad From 969a4b8cfd04d8d60e100cd5fa70692e74286e47 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 13 Oct 2025 13:49:11 +0200 Subject: [PATCH 4/6] Corrected equatable function for AsyncLoad --- Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift b/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift index 65b4004..fb69334 100644 --- a/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift +++ b/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift @@ -5,14 +5,14 @@ public enum AsyncLoad: Equatable, Sendable { case loading case error(Error) case loaded(T) - + public var isLoading: Bool { if case .loading = self { return true } return false } - + public var item: T? { switch self { case let .loaded(item): @@ -21,7 +21,7 @@ public enum AsyncLoad: Equatable, Sendable { nil } } - + public var error: Error? { switch self { case let .error(error): @@ -31,7 +31,7 @@ public enum AsyncLoad: Equatable, Sendable { } } - public static func == (lhs: AsyncLoad, rhs: AsyncLoad) -> Bool { + public static func == (lhs: AsyncLoad, rhs: AsyncLoad) -> Bool where T : Equatable { switch (lhs, rhs) { case (.none, .none): true @@ -39,8 +39,8 @@ public enum AsyncLoad: Equatable, Sendable { true case (.error, .error): true - case (.loaded, .loaded): - true + case let (.loaded(lhsItem), .loaded(rhsItem)): + lhsItem == rhsItem default: false } From d1974d259009b6aeb06e71ed1bc7fed4734b02a1 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 13 Oct 2025 13:52:53 +0200 Subject: [PATCH 5/6] Fixed Xcode selection for macos runner --- .github/workflows/swift-tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift-tests.yaml b/.github/workflows/swift-tests.yaml index 8e1d00d..846fadb 100644 --- a/.github/workflows/swift-tests.yaml +++ b/.github/workflows/swift-tests.yaml @@ -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 From 4c75827cf967097dbfbca8b93648ddd075b24e6f Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 13 Oct 2025 14:41:08 +0200 Subject: [PATCH 6/6] Dropped AsyncAction and CachedAsyncAction --- README.md | 116 +++--------------- Sources/AsyncLoad/AsyncLoad/AsyncAction.swift | 48 -------- .../CachedAsyncLoad/CachedAsyncAction.swift | 48 -------- .../CachedAsyncLoad/CachedAsyncLoadView.swift | 26 ++-- .../AsyncActionEquatableItemTests.swift | 43 ------- .../CachedAsyncActionEquatableItemTests.swift | 48 -------- Tests/AsyncLoadTests/Utils.swift | 20 --- 7 files changed, 29 insertions(+), 320 deletions(-) delete mode 100644 Sources/AsyncLoad/AsyncLoad/AsyncAction.swift delete mode 100644 Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift delete mode 100644 Tests/AsyncLoadTests/AsyncActionEquatableItemTests.swift delete mode 100644 Tests/AsyncLoadTests/CachedAsyncActionEquatableItemTests.swift diff --git a/README.md b/README.md index 46d55fd..0415536 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,7 @@ A Swift package that provides elegant state management for asynchronous operatio AsyncLoad provides components for handling asynchronous operations: - `AsyncLoad`: For loading data operations -- `AsyncAction`: For action-based operations - `CachedAsyncLoad`: For loading operations that preserve cached data during refreshes -- `CachedAsyncAction`: 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 @@ -44,7 +42,7 @@ Or add it through Xcode: An enum that represents the state of an asynchronous data loading operation. ```swift -public enum AsyncLoad: Equatable { +public enum AsyncLoad: Equatable, Sendable { case none // Initial state case loading // Loading in progress case error(Error)// Loading failed with error @@ -52,6 +50,8 @@ public enum AsyncLoad: Equatable { } ``` +> **Note**: The generic type `T` must conform to both `Equatable` and `Sendable` protocols. + #### Properties - `isLoading: Bool` - Returns true if the state is `.loading` @@ -80,53 +80,12 @@ class DataViewModel { } ``` -### AsyncAction - -Similar to AsyncLoad but designed for action-based operations (like posting data, submitting forms, etc.). - -```swift -public enum AsyncAction: 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 = .none - - func submitForm(data: FormData) async { - submitAction = .loading - - do { - let response = try await apiService.submit(data) - submitAction = .success(response) - } catch { - submitAction = .error(error) - } - } -} -``` - ### CachedAsyncLoad An enhanced version of AsyncLoad that preserves cached data during loading and error states. ```swift -public enum CachedAsyncLoad: Equatable { +public enum CachedAsyncLoad: 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 @@ -134,6 +93,8 @@ public enum CachedAsyncLoad: Equatable { } ``` +> **Note**: The generic type `T` must conform to both `Equatable` and `Sendable` protocols. + #### Properties - `isLoading: Bool` - Returns true if the state is `.loading` @@ -169,53 +130,6 @@ class CachedDataViewModel { } ``` -### CachedAsyncAction - -Similar to AsyncAction but preserves cached data during loading and error states. - -```swift -public enum CachedAsyncAction: 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 item from `.success`, `.loading`, or `.error` states, nil for `.none` -- `error: Error?` - Returns the error if state is `.error`, nil otherwise - -#### Example Usage - -```swift -import AsyncLoad - -@Observable -class CachedFormViewModel { - var submitAction: CachedAsyncAction = .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. @@ -354,19 +268,19 @@ struct CachedUserProfileView: View { ## 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) diff --git a/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift b/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift deleted file mode 100644 index e68822d..0000000 --- a/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -public enum AsyncAction: Equatable, Sendable { - case none - case loading - case error(Error) - case success(T) - - public var isLoading: Bool { - if case .loading = self { - return true - } - return false - } - - public var item: T? { - switch self { - case let .success(item): - item - default: - nil - } - } - - public var error: Error? { - switch self { - case let .error(error): - error - default: - nil - } - } - - public static func == (lhs: AsyncAction, rhs: AsyncAction) -> Bool { - 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 - } - } -} diff --git a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift deleted file mode 100644 index 4bc8a7e..0000000 --- a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -public enum CachedAsyncAction: Equatable, Sendable { - case none - case loading(T? = nil) - case error(T? = nil, Error) - case success(T) - - public var isLoading: Bool { - if case .loading = self { - return true - } - return false - } - - public var item: T? { - switch self { - case let .success(item): - item - default: - nil - } - } - - public var error: Error? { - switch self { - case let .error(_, error): - error - default: - nil - } - } - - public static func == (lhs: CachedAsyncAction, rhs: CachedAsyncAction) -> Bool { - 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 - } - } -} diff --git a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift index 2eb717f..9fa1e5e 100644 --- a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift +++ b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift @@ -32,18 +32,20 @@ public struct CachedAsyncLoadView< } public var body: some View { - switch state { - case .none: - loadingContent(nil) - - case let .loading(item): - loadingContent(item) - - case let .loaded(item): - content(item) - - case let .error(item, error): - errorContent(item, error) + ZStack { + switch state { + case .none: + loadingContent(nil) + + case let .loading(item): + loadingContent(item) + + case let .loaded(item): + content(item) + + case let .error(item, error): + errorContent(item, error) + } } } } diff --git a/Tests/AsyncLoadTests/AsyncActionEquatableItemTests.swift b/Tests/AsyncLoadTests/AsyncActionEquatableItemTests.swift deleted file mode 100644 index 602e927..0000000 --- a/Tests/AsyncLoadTests/AsyncActionEquatableItemTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Testing -@testable import AsyncLoad - -@Suite("Test equatable AsyncAction") -struct AsyncActionEquatableItemTests { - @Test<[AsyncActionParameter]>("Should be equal ", arguments: [ - .init(.none, .none), - .init(.loading, .loading), - .init(.error(TestingError.some), .error(TestingError.some)), - .init(.error(TestingError.some), .error(TestingError.other)), - .init(.success("some"), .success("some")), - ]) - func equalString(param: AsyncActionParameter) async throws { - #expect(param.action1 == param.action2) - } - - @Test<[AsyncActionParameter]>("Should not be equal ", arguments: [ - .init(.success("some"), .success("other")), - .init(.error(TestingError.some), .success("other")), - ]) - func nonEqualString(param: AsyncActionParameter) async throws { - #expect(param.action1 != param.action2) - } - - @Test<[AsyncActionParameter]>("Should be equal ", arguments: [ - .init(.none, .none), - .init(.loading, .loading), - .init(.error(TestingError.some), .error(TestingError.some)), - .init(.error(TestingError.some), .error(TestingError.other)), - .init(.success(User(name: "some")), .success(User(name: "some"))), - ]) - func equalUser(param: AsyncActionParameter) async throws { - #expect(param.action1 == param.action2) - } - - @Test<[AsyncActionParameter]>("Should not be equal ", arguments: [ - .init(.success(User(name: "some")), .success(User(name: "other"))), - .init(.error(TestingError.some), .success(User(name: "some"))), - ]) - func nonEqualuser(param: AsyncActionParameter) async throws { - #expect(param.action1 != param.action2) - } -} diff --git a/Tests/AsyncLoadTests/CachedAsyncActionEquatableItemTests.swift b/Tests/AsyncLoadTests/CachedAsyncActionEquatableItemTests.swift deleted file mode 100644 index 25fb8a9..0000000 --- a/Tests/AsyncLoadTests/CachedAsyncActionEquatableItemTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Testing -@testable import AsyncLoad - -@Suite("Test equatable CachedAsyncAction") -struct CachedAsyncActionEquatableItemTests { - @Test<[CachedAsyncActionParameter]>("Should be equal ", arguments: [ - .init(.none, .none), - .init(.loading(), .loading()), - .init(.success("some"), .success("some")), - .init(.loading("some"), .loading("some")), - .init(.error("some", TestingError.some), .error("some", TestingError.some)), - ]) - func equalString(param: CachedAsyncActionParameter) async throws { - #expect(param.action1 == param.action2) - } - - @Test<[CachedAsyncActionParameter]>("Should not be equal ", arguments: [ - .init(.success("Other"), .success("Other1")), - .init(.loading("some"), .loading("other")), - .init(.error(nil, TestingError.some), .error("some", TestingError.some)), - .init(.error(nil, TestingError.some), .error("some", TestingError.other)), - .init(.error("some", TestingError.some), .error(nil, TestingError.some)), - ]) - func nonEqualString(param: CachedAsyncActionParameter) async throws { - #expect(param.action1 != param.action2) - } - - @Test<[CachedAsyncActionParameter]>("Should be equal ", arguments: [ - .init(.none, .none), - .init(.loading(), .loading()), - .init(.success(User(name: "some")), .success(User(name: "some"))), - .init(.loading(User(name: "some")), .loading(User(name: "some"))), - .init(.error(User(name: "some"), TestingError.some), .error(User(name: "some"), TestingError.some)), - ]) - func equalUser(param: CachedAsyncActionParameter) async throws { - #expect(param.action1 == param.action2) - } - - @Test<[CachedAsyncActionParameter]>("Should not be equal ", arguments: [ - .init(.success(User(name: "Alex")), .success(User(name: "Daniel"))), - .init(.loading(User(name: "Alex")), .loading(User(name: "Daniel"))), - .init(.error(nil, TestingError.some), .error(User(name: "some"), TestingError.some)), - .init(.error(User(name: "Alex"), TestingError.some), .error(User(name: "Daniel"), TestingError.some)), - ]) - func nonEqualUser(param: CachedAsyncActionParameter) async throws { - #expect(param.action1 != param.action2) - } -} diff --git a/Tests/AsyncLoadTests/Utils.swift b/Tests/AsyncLoadTests/Utils.swift index 78d4299..7742343 100644 --- a/Tests/AsyncLoadTests/Utils.swift +++ b/Tests/AsyncLoadTests/Utils.swift @@ -18,16 +18,6 @@ struct User: Equatable { let name: String } -struct AsyncActionParameter { - let action1: AsyncAction - let action2: AsyncAction - - init(_ action1: AsyncAction, _ action2: AsyncAction) { - self.action1 = action1 - self.action2 = action2 - } -} - struct AsyncLoadParameter { let load1: AsyncLoad let load2: AsyncLoad @@ -38,16 +28,6 @@ struct AsyncLoadParameter { } } -struct CachedAsyncActionParameter { - let action1: CachedAsyncAction - let action2: CachedAsyncAction - - init(_ action1: CachedAsyncAction, _ action2: CachedAsyncAction) { - self.action1 = action1 - self.action2 = action2 - } -} - struct CachedAsyncLoadParameter { let load1: CachedAsyncLoad let load2: CachedAsyncLoad