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
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,15 @@ struct SearchQueryAtom: StateAtom, Hashable {
}
}

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

if query.isEmpty {
return Just([])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
guard !query.isEmpty else {
return []
}

return api.getSearchMovies(query: query)
.map(\.results)
.eraseToAnyPublisher()
return try await api.getSearchMovies(query: query).results
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ protocol APIClientProtocol {
func getTopRated(page: Int) async throws -> PagedResponse<Movie>
func getUpcoming(page: Int) async throws -> PagedResponse<Movie>
func getCredits(movieID: Int) async throws -> Credits
func getSearchMovies(query: String) -> Future<PagedResponse<Movie>, Error> // Use Publisher as an example.
func getSearchMovies(query: String) async throws -> PagedResponse<Movie>
}

struct APIClient: APIClientProtocol {
Expand All @@ -30,7 +30,7 @@ struct APIClient: APIClientProtocol {
imageBaseURL
.appendingPathComponent(size.rawValue)
.appendingPathComponent(path)
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5)
let (data, _) = try await session.data(for: request)
return UIImage(data: data) ?? UIImage()
}
Expand Down Expand Up @@ -58,24 +58,6 @@ struct APIClient: APIClientProtocol {
func getSearchMovies(query: String) async throws -> PagedResponse<Movie> {
try await get(path: "search/movie", parameters: ["query": query])
}

func getSearchMovies(query: String) -> Future<PagedResponse<Movie>, Error> {
Future { fulfill in
Task {
do {
let response = try await get(
PagedResponse<Movie>.self,
path: "search/movie",
parameters: ["query": query]
)
fulfill(.success(response))
}
catch {
fulfill(.failure(error))
}
}
}
}
}

private extension APIClient {
Expand Down Expand Up @@ -142,9 +124,7 @@ final class MockAPIClient: APIClientProtocol {
try creditsResponse.get()
}

func getSearchMovies(query: String) -> Future<PagedResponse<Movie>, Error> {
Future { fulfill in
fulfill(self.searchMoviesResponse)
}
func getSearchMovies(query: String) async throws -> PagedResponse<Movie> {
try searchMoviesResponse.get()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Atoms
import SwiftUI

struct SearchScreen: View {
@Watch(SearchMoviesAtom())
@Watch(SearchMoviesAtom().phase)
var movies

@ViewContext
Expand Down Expand Up @@ -36,7 +36,7 @@ struct SearchScreen: View {
.navigationTitle("Search Results")
.listStyle(.insetGrouped)
.refreshable {
await context.refresh(SearchMoviesAtom())
await context.refresh(SearchMoviesAtom().phase)
}
.sheet(item: $selectedMovie) { movie in
DetailScreen(movie: movie)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,33 +113,37 @@ final class ExampleMovieDBTests: XCTestCase {
XCTAssertEqual(failurePhase.error as? URLError, error)
}

func testSearchMoviesAtom() async {
func testSearchMoviesAtom() async throws {
let apiClient = MockAPIClient()
let atom = SearchMoviesAtom()
let context = AtomTestContext()
let expected = PagedResponse.stub()
let error = URLError(.badURL)
let errorError = URLError(.badURL)

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

context.watch(SearchQueryAtom())

let emptyQueryPhase = await context.refresh(atom)
let empty = try await context.refresh(atom).value

XCTAssertEqual(emptyQueryPhase.value, [])
XCTAssertEqual(empty, [])

context[SearchQueryAtom()] = "query"

let successPhase = await context.refresh(atom)

XCTAssertEqual(successPhase.value, expected.results)
let success = try await context.refresh(atom).value

apiClient.searchMoviesResponse = .failure(error)
XCTAssertEqual(success, expected.results)

let failurePhase = await context.refresh(atom)
apiClient.searchMoviesResponse = .failure(errorError)

XCTAssertEqual(failurePhase.error as? URLError, error)
do {
_ = try await context.refresh(atom).value
XCTFail("Should throw.")
}
catch {
XCTAssertEqual(error as? URLError, errorError)
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/Atoms/Core/Loader/AsyncSequenceAtomLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public struct AsyncSequenceAtomLoader<Node: AsyncSequenceAtom>: RefreshableAtomL
value
}

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

/// Refreshes and awaits for the passed value to be finished to yield values
/// Refreshes and waits for the passed value to finish outputting values
/// and returns a final value.
public func refreshOverridden(value: Value, context: Context) async -> Value {
public func refresh(overridden value: Value, context: Context) async -> Value {
value
}
}
6 changes: 3 additions & 3 deletions Sources/Atoms/Core/Loader/AtomLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ public extension AtomLoader {
/// A loader protocol that represents an actual implementation of the corresponding atom
/// that provides values asynchronously.
public protocol RefreshableAtomLoader: AtomLoader {
/// Refreshes and awaits until the asynchronous is finished and returns a final value.
/// Refreshes and waits until the asynchronous process is finished and returns a final value.
func refresh(context: Context) async -> Value

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

/// A loader protocol that represents an actual implementation of the corresponding atom
Expand Down
17 changes: 17 additions & 0 deletions Sources/Atoms/Core/Loader/ModifiedAtomLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,20 @@ public struct ModifiedAtomLoader<Node: Atom, Modifier: AtomModifier>: AtomLoader
modifier.shouldUpdate(newValue: newValue, oldValue: oldValue)
}
}

extension ModifiedAtomLoader: RefreshableAtomLoader where Node.Loader: RefreshableAtomLoader, Modifier: RefreshableAtomModifier {
/// Refreshes and waits until the asynchronous process is finished and returns a final value.
public func refresh(context: Context) async -> Value {
let value = await context.transaction { context in
await context.refresh(atom)
return context.watch(atom)
}
return await modifier.refresh(modifying: value, context: context.modifierContext)
}

/// Refreshes and waits for the passed value to finish outputting values
/// and returns a final value.
public func refresh(overridden value: Value, context: Context) async -> Value {
await modifier.refresh(overridden: value, context: context.modifierContext)
}
}
6 changes: 3 additions & 3 deletions Sources/Atoms/Core/Loader/PublisherAtomLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public struct PublisherAtomLoader<Node: PublisherAtom>: RefreshableAtomLoader {
value
}

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

/// Refreshes and awaits for the passed value to be finished to yield values
/// Refreshes and waits for the passed value to finish outputting values
/// and returns a final value.
public func refreshOverridden(value: Value, context: Context) async -> Value {
public func refresh(overridden value: Value, context: Context) async -> Value {
value
}
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/Atoms/Core/Loader/TaskAtomLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ public struct TaskAtomLoader<Node: TaskAtom>: AsyncAtomLoader {
return value
}

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

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

return await withTaskCancellationHandler {
Expand Down
8 changes: 4 additions & 4 deletions Sources/Atoms/Core/Loader/ThrowingTaskAtomLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ public struct ThrowingTaskAtomLoader<Node: ThrowingTaskAtom>: AsyncAtomLoader {
return value
}

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

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

return await withTaskCancellationHandler {
Expand Down
Loading