Skip to content

Commit 7bf5d0b

Browse files
authored
Merge pull request #2 from ra1028/improve-async-status
Improve AsyncStatus
2 parents 00a61c2 + be24220 commit 7bf5d0b

File tree

5 files changed

+232
-25
lines changed

5 files changed

+232
-25
lines changed

Examples/TheMovieDB/CustomHooks/UseFetchPage.swift

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,10 @@ func useFetchPage<Response: Decodable>(
1717
results.current += response.results
1818
}
1919

20-
guard page.current == 0 else {
21-
return (status: .success(results.current), fetch: fetch)
22-
}
23-
24-
switch status {
25-
case .pending:
26-
return (status: .pending, fetch: fetch)
27-
28-
case .running:
29-
return (status: .running, fetch: fetch)
20+
let newStatus =
21+
page.current == 0
22+
? status.map { _ in results.current }
23+
: .success(results.current)
3024

31-
case .success:
32-
return (status: .success(results.current), fetch: fetch)
33-
34-
case .failure(let error):
35-
return (status: .failure(error), fetch: fetch)
36-
}
25+
return (status: newStatus, fetch: fetch)
3726
}

Examples/TheMovieDB/CustomHooks/UseNetworkImage.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,5 @@ func useNetworkImage(for path: String, size: NetworkImageSize) -> UIImage? {
1414
.receive(on: DispatchQueue.main)
1515
}
1616

17-
switch status {
18-
case .success(let image):
19-
return image
20-
21-
case .failure, .pending, .running:
22-
return nil
23-
}
17+
return try? status.get() ?? nil
2418
}

Examples/TheMovieDB/TopRatedMoviesPage.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct TopRatedMoviesPage: HookView {
1818
failure(error, onReload: fetch)
1919

2020
case .pending, .running:
21-
ProgressView()
21+
loading
2222
}
2323
}
2424
.navigationTitle("Top Rated Movies")
@@ -31,6 +31,10 @@ struct TopRatedMoviesPage: HookView {
3131
.onAppear(perform: fetch)
3232
}
3333

34+
var loading: some View {
35+
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
36+
}
37+
3438
func failure(_ error: URLError, onReload: @escaping () -> Void) -> some View {
3539
VStack(spacing: 16) {
3640
Text("Failed to fetch movies")

Sources/Hooks/AsyncStatus.swift

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,97 @@ public enum AsyncStatus<Success, Failure: Error> {
1313
/// Represents a success status meaning that the operation provided an error with failure.
1414
case failure(Failure)
1515

16-
/// Returns a result converted from the status. If the status is `pending` or `running`, it returns nil.
16+
/// Returns a Boolean value indicating whether this instance represents a `running`.
17+
public var isRunning: Bool {
18+
guard case .running = self else {
19+
return false
20+
}
21+
return true
22+
}
23+
24+
/// Returns a result converted from the status.
25+
/// If this instance represents a `pending` or a `running`, this returns nil.
1726
public var result: Result<Success, Failure>? {
1827
switch self {
28+
case .pending, .running:
29+
return nil
30+
1931
case .success(let success):
2032
return .success(success)
2133

2234
case .failure(let error):
2335
return .failure(error)
36+
}
37+
}
2438

39+
/// Returns a new status, mapping any success value using the given transformation.
40+
/// - Parameter transform: A closure that takes the success value of this instance.
41+
/// - Returns: An `AsyncStatus` instance with the result of evaluating `transform` as the new success value if this instance represents a success.
42+
public func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> AsyncStatus<NewSuccess, Failure> {
43+
flatMap { .success(transform($0)) }
44+
}
45+
46+
/// Returns a new result, mapping any failure value using the given transformation.
47+
/// - Parameter transform: A closure that takes the failure value of the instance.
48+
/// - Returns: An `AsyncStatus` instance with the result of evaluating `transform` as the new failure value if this instance represents a failure.
49+
public func mapError<NewFailure: Error>(_ transform: (Failure) -> NewFailure) -> AsyncStatus<Success, NewFailure> {
50+
flatMapError { .failure(transform($0)) }
51+
}
52+
53+
/// Returns a new result, mapping any success value using the given transformation and unwrapping the produced status.
54+
/// - Parameter transform: A closure that takes the success value of the instance.
55+
/// - Returns: An `AsyncStatus` instance, either from the closure or the previous `.success`.
56+
public func flatMap<NewSuccess>(_ transform: (Success) -> AsyncStatus<NewSuccess, Failure>) -> AsyncStatus<NewSuccess, Failure> {
57+
switch self {
58+
case .pending:
59+
return .pending
60+
61+
case .running:
62+
return .running
63+
64+
case .success(let value):
65+
return transform(value)
66+
67+
case .failure(let error):
68+
return .failure(error)
69+
}
70+
}
71+
72+
/// Returns a new result, mapping any failure value using the given transformation and unwrapping the produced status.
73+
/// - Parameter transform: A closure that takes the failure value of the instance.
74+
/// - Returns: An `AsyncStatus` instance, either from the closure or the previous `.failure`.
75+
public func flatMapError<NewFailure: Error>(_ transform: (Failure) -> AsyncStatus<Success, NewFailure>) -> AsyncStatus<Success, NewFailure> {
76+
switch self {
77+
case .pending:
78+
return .pending
79+
80+
case .running:
81+
return .running
82+
83+
case .success(let value):
84+
return .success(value)
85+
86+
case .failure(let error):
87+
return transform(error)
88+
}
89+
}
90+
91+
/// Returns the success value as a throwing expression.
92+
/// If this instance represents a `pending` or a `running`, this returns nil.
93+
///
94+
/// Use this method to retrieve the value of this status if it represents a success, or to catch the value if it represents a failure.
95+
/// - Throws: The failure value, if the instance represents a failure.
96+
/// - Returns: The success value, if the instance represents a success,If the status is `pending` or `running`, this returns nil. .
97+
public func get() throws -> Success? {
98+
switch self {
2599
case .pending, .running:
26100
return nil
101+
102+
case .success(let value):
103+
return value
104+
105+
case .failure(let error):
106+
throw error
27107
}
28108
}
29109
}

Tests/AsyncStatusTests.swift

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@ import Hooks
22
import XCTest
33

44
final class AsyncStatusTests: XCTestCase {
5+
func testIsRunning() {
6+
let statuses: [AsyncStatus<Int, URLError>] = [
7+
.pending,
8+
.running,
9+
.success(0),
10+
.failure(URLError(.badURL)),
11+
]
12+
13+
let expected = [
14+
false,
15+
true,
16+
false,
17+
false,
18+
]
19+
20+
for (status, expected) in zip(statuses, expected) {
21+
XCTAssertEqual(status.isRunning, expected)
22+
}
23+
}
24+
525
func testResult() {
626
let statuses: [AsyncStatus<Int, URLError>] = [
727
.pending,
@@ -21,4 +41,124 @@ final class AsyncStatusTests: XCTestCase {
2141
XCTAssertEqual(status.result, expected)
2242
}
2343
}
44+
45+
func testMap() {
46+
let statuses: [AsyncStatus<Int, URLError>] = [
47+
.pending,
48+
.running,
49+
.success(0),
50+
.failure(URLError(.badURL)),
51+
]
52+
53+
let expected: [AsyncStatus<Int, URLError>] = [
54+
.pending,
55+
.running,
56+
.success(100),
57+
.failure(URLError(.badURL)),
58+
]
59+
60+
for (status, expected) in zip(statuses, expected) {
61+
XCTAssertEqual(status.map { _ in 100 }, expected)
62+
}
63+
}
64+
65+
func testMapError() {
66+
let statuses: [AsyncStatus<Int, URLError>] = [
67+
.pending,
68+
.running,
69+
.success(0),
70+
.failure(URLError(.badURL)),
71+
]
72+
73+
let expected: [AsyncStatus<Int, URLError>] = [
74+
.pending,
75+
.running,
76+
.success(0),
77+
.failure(URLError(.cancelled)),
78+
]
79+
80+
for (status, expected) in zip(statuses, expected) {
81+
XCTAssertEqual(
82+
status.mapError { _ in URLError(.cancelled) },
83+
expected
84+
)
85+
}
86+
}
87+
88+
func testFlatMap() {
89+
let statuses: [AsyncStatus<Int, URLError>] = [
90+
.pending,
91+
.running,
92+
.success(0),
93+
.failure(URLError(.badURL)),
94+
]
95+
96+
let expected: [AsyncStatus<Int, URLError>] = [
97+
.pending,
98+
.running,
99+
.failure(URLError(.callIsActive)),
100+
.failure(URLError(.badURL)),
101+
]
102+
103+
for (status, expected) in zip(statuses, expected) {
104+
XCTAssertEqual(
105+
status.flatMap { _ in .failure(URLError(.callIsActive)) },
106+
expected
107+
)
108+
}
109+
}
110+
111+
func testFlatMapError() {
112+
let statuses: [AsyncStatus<Int, URLError>] = [
113+
.pending,
114+
.running,
115+
.success(0),
116+
.failure(URLError(.badURL)),
117+
]
118+
119+
let expected: [AsyncStatus<Int, URLError>] = [
120+
.pending,
121+
.running,
122+
.success(0),
123+
.success(100),
124+
]
125+
126+
for (status, expected) in zip(statuses, expected) {
127+
XCTAssertEqual(
128+
status.flatMapError { _ in .success(100) },
129+
expected
130+
)
131+
}
132+
}
133+
134+
func testGet() throws {
135+
let statuses: [AsyncStatus<Int, URLError>] = [
136+
.pending,
137+
.running,
138+
.success(100),
139+
]
140+
141+
let expected: [Int?] = [
142+
nil,
143+
nil,
144+
100,
145+
nil,
146+
]
147+
148+
for (status, expected) in zip(statuses, expected) {
149+
XCTAssertEqual(
150+
try status.get(),
151+
expected
152+
)
153+
}
154+
155+
do {
156+
let error = URLError(.badServerResponse)
157+
_ = try AsyncStatus<Int, URLError>.failure(error).get()
158+
}
159+
catch {
160+
let error = error as? URLError
161+
XCTAssertEqual(error, URLError(.badServerResponse))
162+
}
163+
}
24164
}

0 commit comments

Comments
 (0)