Skip to content

Commit 4adcd5c

Browse files
authored
Add ability to refresh modified atoms (#91)
* Add test case * Add ability to refresh modified atoms * Update example * Update documentation comments * Refactoring * Fix tests
1 parent c76edf2 commit 4adcd5c

15 files changed

+148
-113
lines changed

Examples/Packages/iOS/Sources/ExampleMovieDB/Atoms/SearchAtoms.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,15 @@ struct SearchQueryAtom: StateAtom, Hashable {
77
}
88
}
99

10-
struct SearchMoviesAtom: PublisherAtom, Hashable {
11-
func publisher(context: Context) -> AnyPublisher<[Movie], Error> {
10+
struct SearchMoviesAtom: ThrowingTaskAtom, Hashable {
11+
func value(context: Context) async throws -> [Movie] {
1212
let api = context.watch(APIClientAtom())
1313
let query = context.watch(SearchQueryAtom())
1414

15-
if query.isEmpty {
16-
return Just([])
17-
.setFailureType(to: Error.self)
18-
.eraseToAnyPublisher()
15+
guard !query.isEmpty else {
16+
return []
1917
}
2018

21-
return api.getSearchMovies(query: query)
22-
.map(\.results)
23-
.eraseToAnyPublisher()
19+
return try await api.getSearchMovies(query: query).results
2420
}
2521
}

Examples/Packages/iOS/Sources/ExampleMovieDB/Backend/APIClient.swift

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ protocol APIClientProtocol {
88
func getTopRated(page: Int) async throws -> PagedResponse<Movie>
99
func getUpcoming(page: Int) async throws -> PagedResponse<Movie>
1010
func getCredits(movieID: Int) async throws -> Credits
11-
func getSearchMovies(query: String) -> Future<PagedResponse<Movie>, Error> // Use Publisher as an example.
11+
func getSearchMovies(query: String) async throws -> PagedResponse<Movie>
1212
}
1313

1414
struct APIClient: APIClientProtocol {
@@ -30,7 +30,7 @@ struct APIClient: APIClientProtocol {
3030
imageBaseURL
3131
.appendingPathComponent(size.rawValue)
3232
.appendingPathComponent(path)
33-
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
33+
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5)
3434
let (data, _) = try await session.data(for: request)
3535
return UIImage(data: data) ?? UIImage()
3636
}
@@ -58,24 +58,6 @@ struct APIClient: APIClientProtocol {
5858
func getSearchMovies(query: String) async throws -> PagedResponse<Movie> {
5959
try await get(path: "search/movie", parameters: ["query": query])
6060
}
61-
62-
func getSearchMovies(query: String) -> Future<PagedResponse<Movie>, Error> {
63-
Future { fulfill in
64-
Task {
65-
do {
66-
let response = try await get(
67-
PagedResponse<Movie>.self,
68-
path: "search/movie",
69-
parameters: ["query": query]
70-
)
71-
fulfill(.success(response))
72-
}
73-
catch {
74-
fulfill(.failure(error))
75-
}
76-
}
77-
}
78-
}
7961
}
8062

8163
private extension APIClient {
@@ -142,9 +124,7 @@ final class MockAPIClient: APIClientProtocol {
142124
try creditsResponse.get()
143125
}
144126

145-
func getSearchMovies(query: String) -> Future<PagedResponse<Movie>, Error> {
146-
Future { fulfill in
147-
fulfill(self.searchMoviesResponse)
148-
}
127+
func getSearchMovies(query: String) async throws -> PagedResponse<Movie> {
128+
try searchMoviesResponse.get()
149129
}
150130
}

Examples/Packages/iOS/Sources/ExampleMovieDB/Screens/SearchScreen.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Atoms
22
import SwiftUI
33

44
struct SearchScreen: View {
5-
@Watch(SearchMoviesAtom())
5+
@Watch(SearchMoviesAtom().phase)
66
var movies
77

88
@ViewContext
@@ -36,7 +36,7 @@ struct SearchScreen: View {
3636
.navigationTitle("Search Results")
3737
.listStyle(.insetGrouped)
3838
.refreshable {
39-
await context.refresh(SearchMoviesAtom())
39+
await context.refresh(SearchMoviesAtom().phase)
4040
}
4141
.sheet(item: $selectedMovie) { movie in
4242
DetailScreen(movie: movie)

Examples/Packages/iOS/Tests/ExampleMovieDBTests/ExampleMovieDBTests.swift

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,33 +113,37 @@ final class ExampleMovieDBTests: XCTestCase {
113113
XCTAssertEqual(failurePhase.error as? URLError, error)
114114
}
115115

116-
func testSearchMoviesAtom() async {
116+
func testSearchMoviesAtom() async throws {
117117
let apiClient = MockAPIClient()
118118
let atom = SearchMoviesAtom()
119119
let context = AtomTestContext()
120120
let expected = PagedResponse.stub()
121-
let error = URLError(.badURL)
121+
let errorError = URLError(.badURL)
122122

123123
context.override(APIClientAtom()) { _ in apiClient }
124124
apiClient.searchMoviesResponse = .success(expected)
125125

126126
context.watch(SearchQueryAtom())
127127

128-
let emptyQueryPhase = await context.refresh(atom)
128+
let empty = try await context.refresh(atom).value
129129

130-
XCTAssertEqual(emptyQueryPhase.value, [])
130+
XCTAssertEqual(empty, [])
131131

132132
context[SearchQueryAtom()] = "query"
133133

134-
let successPhase = await context.refresh(atom)
135-
136-
XCTAssertEqual(successPhase.value, expected.results)
134+
let success = try await context.refresh(atom).value
137135

138-
apiClient.searchMoviesResponse = .failure(error)
136+
XCTAssertEqual(success, expected.results)
139137

140-
let failurePhase = await context.refresh(atom)
138+
apiClient.searchMoviesResponse = .failure(errorError)
141139

142-
XCTAssertEqual(failurePhase.error as? URLError, error)
140+
do {
141+
_ = try await context.refresh(atom).value
142+
XCTFail("Should throw.")
143+
}
144+
catch {
145+
XCTAssertEqual(error as? URLError, errorError)
146+
}
143147
}
144148
}
145149

Sources/Atoms/Core/Loader/AsyncSequenceAtomLoader.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public struct AsyncSequenceAtomLoader<Node: AsyncSequenceAtom>: RefreshableAtomL
4040
value
4141
}
4242

43-
/// Refreshes and awaits for the passed value to be finished to yield values
43+
/// Refreshes and waits for the passed value to finish outputting values
4444
/// and returns a final value.
4545
public func refresh(context: Context) async -> Value {
4646
let sequence = context.transaction(atom.sequence)
@@ -70,9 +70,9 @@ public struct AsyncSequenceAtomLoader<Node: AsyncSequenceAtom>: RefreshableAtomL
7070
}
7171
}
7272

73-
/// Refreshes and awaits for the passed value to be finished to yield values
73+
/// Refreshes and waits for the passed value to finish outputting values
7474
/// and returns a final value.
75-
public func refreshOverridden(value: Value, context: Context) async -> Value {
75+
public func refresh(overridden value: Value, context: Context) async -> Value {
7676
value
7777
}
7878
}

Sources/Atoms/Core/Loader/AtomLoader.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ public extension AtomLoader {
3030
/// A loader protocol that represents an actual implementation of the corresponding atom
3131
/// that provides values asynchronously.
3232
public protocol RefreshableAtomLoader: AtomLoader {
33-
/// Refreshes and awaits until the asynchronous is finished and returns a final value.
33+
/// Refreshes and waits until the asynchronous process is finished and returns a final value.
3434
func refresh(context: Context) async -> Value
3535

36-
/// Refreshes and awaits for the passed value to be finished to yield values
36+
/// Refreshes and waits for the passed value to finish outputting values
3737
/// and returns a final value.
38-
func refreshOverridden(value: Value, context: Context) async -> Value
38+
func refresh(overridden value: Value, context: Context) async -> Value
3939
}
4040

4141
/// A loader protocol that represents an actual implementation of the corresponding atom

Sources/Atoms/Core/Loader/ModifiedAtomLoader.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,20 @@ public struct ModifiedAtomLoader<Node: Atom, Modifier: AtomModifier>: AtomLoader
3131
modifier.shouldUpdate(newValue: newValue, oldValue: oldValue)
3232
}
3333
}
34+
35+
extension ModifiedAtomLoader: RefreshableAtomLoader where Node.Loader: RefreshableAtomLoader, Modifier: RefreshableAtomModifier {
36+
/// Refreshes and waits until the asynchronous process is finished and returns a final value.
37+
public func refresh(context: Context) async -> Value {
38+
let value = await context.transaction { context in
39+
await context.refresh(atom)
40+
return context.watch(atom)
41+
}
42+
return await modifier.refresh(modifying: value, context: context.modifierContext)
43+
}
44+
45+
/// Refreshes and waits for the passed value to finish outputting values
46+
/// and returns a final value.
47+
public func refresh(overridden value: Value, context: Context) async -> Value {
48+
await modifier.refresh(overridden: value, context: context.modifierContext)
49+
}
50+
}

Sources/Atoms/Core/Loader/PublisherAtomLoader.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public struct PublisherAtomLoader<Node: PublisherAtom>: RefreshableAtomLoader {
3535
value
3636
}
3737

38-
/// Refreshes and awaits until the asynchronous is finished and returns a final value.
38+
/// Refreshes and waits until the asynchronous process is finished and returns a final value.
3939
public func refresh(context: Context) async -> Value {
4040
let results = context.transaction(atom.publisher).results
4141
let task = Task {
@@ -59,9 +59,9 @@ public struct PublisherAtomLoader<Node: PublisherAtom>: RefreshableAtomLoader {
5959
}
6060
}
6161

62-
/// Refreshes and awaits for the passed value to be finished to yield values
62+
/// Refreshes and waits for the passed value to finish outputting values
6363
/// and returns a final value.
64-
public func refreshOverridden(value: Value, context: Context) async -> Value {
64+
public func refresh(overridden value: Value, context: Context) async -> Value {
6565
value
6666
}
6767
}

Sources/Atoms/Core/Loader/TaskAtomLoader.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,17 @@ public struct TaskAtomLoader<Node: TaskAtom>: AsyncAtomLoader {
3232
return value
3333
}
3434

35-
/// Refreshes and awaits until the asynchronous is finished and returns a final value.
35+
/// Refreshes and waits until the asynchronous process is finished and returns a final value.
3636
public func refresh(context: Context) async -> Value {
3737
let task = Task {
3838
await context.transaction(atom.value)
3939
}
40-
return await refreshOverridden(value: task, context: context)
40+
return await refresh(overridden: task, context: context)
4141
}
4242

43-
/// Refreshes and awaits for the passed value to be finished to yield values
43+
/// Refreshes and waits for the passed value to finish outputting values
4444
/// and returns a final value.
45-
public func refreshOverridden(value: Value, context: Context) async -> Value {
45+
public func refresh(overridden value: Value, context: Context) async -> Value {
4646
context.addTermination(value.cancel)
4747

4848
return await withTaskCancellationHandler {

Sources/Atoms/Core/Loader/ThrowingTaskAtomLoader.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,17 @@ public struct ThrowingTaskAtomLoader<Node: ThrowingTaskAtom>: AsyncAtomLoader {
3232
return value
3333
}
3434

35-
/// Refreshes and awaits until the asynchronous is finished and returns a final value.
35+
/// Refreshes and waits until the asynchronous process is finished and returns a final value.
3636
public func refresh(context: Context) async -> Value {
3737
let task = Task {
3838
try await context.transaction(atom.value)
3939
}
40-
return await refreshOverridden(value: task, context: context)
40+
return await refresh(overridden: task, context: context)
4141
}
4242

43-
/// Refreshes and awaits for the passed value to be finished to yield values
43+
/// Refreshes and waits for the passed value to finish outputting values
4444
/// and returns a final value.
45-
public func refreshOverridden(value: Value, context: Context) async -> Value {
45+
public func refresh(overridden value: Value, context: Context) async -> Value {
4646
context.addTermination(value.cancel)
4747

4848
return await withTaskCancellationHandler {

0 commit comments

Comments
 (0)