-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/tca-reducer-protocol' into develop
- Loading branch information
Showing
14 changed files
with
423 additions
and
453 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,4 +89,5 @@ fastlane/test_output | |
|
||
iOSInjectionProject/ | ||
|
||
.idea/ | ||
.idea/ | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,96 +1,99 @@ | ||
import Foundation | ||
import ComposableArchitecture | ||
|
||
struct ForecastState: Equatable { | ||
var followingList: [CityViewModel] = [] | ||
struct ForecastReducer: ReducerProtocol { | ||
|
||
var forecast: [Int: OneCall]? | ||
var loadingCityIDSet: Set<Int> = [] | ||
} | ||
|
||
enum ForecastAction: Equatable { | ||
case fetchFollowingCity | ||
case fetchFollowingCityDone(Result<[CityViewModel], AppError>) | ||
|
||
case follow(city: CityViewModel) | ||
case followDone(Result<CityViewModel, Never>) | ||
case unfollowCity(indexSet: IndexSet) | ||
case unfollowCityDone(Result<CityViewModel, Never>) | ||
case moveCity(indexSet: IndexSet, toIndex: Int) | ||
struct State: Equatable { | ||
var followingList: [CityViewModel] = [] | ||
|
||
var forecast: [Int: OneCall]? | ||
var loadingCityIDSet: Set<Int> = [] | ||
} | ||
|
||
case loadCityForecast(city: CityViewModel) | ||
case loadCityForecastDone(cityID: Int, result: Result<OneCall, AppError>) | ||
} | ||
|
||
struct ForecastEnvironment { | ||
var mainQueue: AnySchedulerOf<DispatchQueue> | ||
var weatherClient: WeatherClient | ||
var followingClient: FollowingClient | ||
} | ||
|
||
let forecastReducer = Reducer<ForecastState, ForecastAction, ForecastEnvironment> { | ||
state, action, environment in | ||
enum Action: Equatable { | ||
case fetchFollowingCity | ||
case fetchFollowingCityDone(TaskResult<[CityViewModel]>) | ||
|
||
case follow(city: CityViewModel) | ||
case followDone(TaskResult<CityViewModel>) | ||
case unfollowCity(indexSet: IndexSet) | ||
case unfollowCityDone(TaskResult<CityViewModel>) | ||
case moveCity(indexSet: IndexSet, toIndex: Int) | ||
|
||
case loadCityForecast(city: CityViewModel) | ||
case loadCityForecastDone(cityID: Int, result: TaskResult<OneCall>) | ||
} | ||
|
||
switch action { | ||
case .fetchFollowingCity: | ||
return environment.followingClient | ||
.fetch() | ||
.receive(on: environment.mainQueue) | ||
.catchToEffect(ForecastAction.fetchFollowingCityDone) | ||
case .fetchFollowingCityDone(let result): | ||
switch result { | ||
case .success(let list): | ||
state.followingList = list | ||
case .failure(let error): | ||
customDump(error) | ||
} | ||
return .none | ||
case .follow(var city): | ||
city.index = Int((state.followingList.last?.index ?? 0) + 1) | ||
return environment.followingClient | ||
.save(city) | ||
.receive(on: environment.mainQueue) | ||
.catchToEffect(ForecastAction.followDone) | ||
case .followDone(let result): | ||
if case let .success(city) = result { | ||
state.followingList.append(city) | ||
} | ||
return .none | ||
case .unfollowCity(let indexSet): | ||
return environment.followingClient | ||
.delete(state.followingList[indexSet.first!]) | ||
.receive(on: environment.mainQueue) | ||
.catchToEffect(ForecastAction.unfollowCityDone) | ||
case .unfollowCityDone(let result): | ||
if case let .success(city) = result { | ||
state.followingList.removeAll(where: { $0.id == city.id }) | ||
} | ||
return .none | ||
case let .moveCity(indexSet, toIndex): | ||
return environment.followingClient | ||
.move(state.followingList, indexSet, toIndex) | ||
.receive(on: environment.mainQueue) | ||
.catchToEffect(ForecastAction.fetchFollowingCityDone) | ||
case .loadCityForecast(city: let city): | ||
guard !state.loadingCityIDSet.contains(city.id) else { return .none } | ||
state.loadingCityIDSet.insert(city.id) | ||
return environment.weatherClient | ||
.oneCall(city.coord.lat, city.coord.lon) | ||
.receive(on: environment.mainQueue) | ||
.catchToEffect { result in | ||
ForecastAction.loadCityForecastDone(cityID: city.id, result: result) | ||
@Dependency(\.weatherClient) var weatherClient | ||
@Dependency(\.followingClient) var followingClient | ||
|
||
var body: some ReducerProtocol<State, Action> { | ||
Reduce { state, action in | ||
switch action { | ||
case .fetchFollowingCity: | ||
return .task { | ||
await .fetchFollowingCityDone(TaskResult<[CityViewModel]> { | ||
try await followingClient.fetch() | ||
}) | ||
} | ||
case .fetchFollowingCityDone(let result): | ||
switch result { | ||
case .success(let list): | ||
state.followingList = list | ||
case .failure(let error): | ||
customDump(error) | ||
} | ||
return .none | ||
case .follow(let city): | ||
return .task { [lastIndex = state.followingList.last?.index ?? 0] in | ||
await .followDone(TaskResult<CityViewModel> { | ||
var city = city | ||
city.index = Int(lastIndex + 1) | ||
return try followingClient.save(city) | ||
}) | ||
} | ||
case .followDone(let result): | ||
if case let .success(city) = result { | ||
state.followingList.append(city) | ||
} | ||
return .none | ||
case .unfollowCity(let indexSet): | ||
return .task { [city = state.followingList[indexSet.first!]] in | ||
await .unfollowCityDone(TaskResult<CityViewModel> { | ||
try followingClient.delete(city) | ||
}) | ||
} | ||
case .unfollowCityDone(let result): | ||
if case let .success(city) = result { | ||
state.followingList.removeAll(where: { $0.id == city.id }) | ||
} | ||
return .none | ||
case let .moveCity(indexSet, toIndex): | ||
return .task { [followingList = state.followingList] in | ||
await .fetchFollowingCityDone(TaskResult<[CityViewModel]> { | ||
try followingClient.move(followingList, indexSet, toIndex) | ||
}) | ||
} | ||
case .loadCityForecast(city: let city): | ||
guard !state.loadingCityIDSet.contains(city.id) else { return .none } | ||
state.loadingCityIDSet.insert(city.id) | ||
return .task { | ||
await .loadCityForecastDone(cityID: city.id, result: TaskResult<OneCall> { | ||
try await weatherClient.oneCall(city.coord.lat, city.coord.lon) | ||
}) | ||
} | ||
case .loadCityForecastDone(cityID: let cityID, result: let result): | ||
state.loadingCityIDSet.remove(cityID) | ||
switch result { | ||
case .success(let value): | ||
var forecast = state.forecast ?? [:] | ||
forecast[cityID] = value | ||
state.forecast = forecast | ||
case .failure(let error): | ||
dump(error) | ||
} | ||
return .none | ||
} | ||
case .loadCityForecastDone(cityID: let cityID, result: let result): | ||
state.loadingCityIDSet.remove(cityID) | ||
switch result { | ||
case .success(let value): | ||
var forecast = state.forecast ?? [:] | ||
forecast[cityID] = value | ||
state.forecast = forecast | ||
case .failure(let error): | ||
dump(error) | ||
} | ||
return .none | ||
} | ||
} | ||
.debug() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,62 @@ | ||
import Foundation | ||
import ComposableArchitecture | ||
|
||
struct SearchState: Equatable { | ||
struct SearchReducer: ReducerProtocol { | ||
|
||
@BindableState var searchQuery = "" | ||
var list: [Find.City] = [] | ||
var status: Status = .normal | ||
|
||
@BindableState var selectedCity: CityViewModel? | ||
struct State: Equatable { | ||
|
||
@BindableState var searchQuery = "" | ||
var list: [Find.City] = [] | ||
var status: Status = .normal | ||
|
||
@BindableState var selectedCity: CityViewModel? | ||
|
||
enum Status: Equatable { | ||
case normal, loading, noResult, failed(String) | ||
} | ||
} | ||
|
||
enum Status: Equatable { | ||
case normal, loading, noResult, failed(String) | ||
enum Action: Equatable, BindableAction { | ||
case binding(BindingAction<State>) | ||
case search(query: String) | ||
case citiesResponse(TaskResult<[Find.City]>) | ||
} | ||
} | ||
|
||
enum SearchAction: Equatable, BindableAction { | ||
case binding(BindingAction<SearchState>) | ||
case search(query: String) | ||
case citiesResponse(Result<[Find.City], AppError>) | ||
} | ||
|
||
struct SearchEnvironment { | ||
var mainQueue: AnySchedulerOf<DispatchQueue> | ||
var weatherClient: WeatherClient | ||
} | ||
|
||
let searchReducer = Reducer<SearchState, SearchAction, SearchEnvironment> { | ||
state, action, environment in | ||
|
||
switch action { | ||
case .binding(let action): | ||
if action.keyPath == \.$searchQuery, state.searchQuery.count == 0 { | ||
state.status = .normal | ||
state.list = [] | ||
} | ||
return .none | ||
case .search(let query): | ||
struct SearchCityId: Hashable { } | ||
|
||
guard state.status != .loading else { return .none } | ||
state.status = .loading | ||
return environment.weatherClient | ||
.searchCity(query) | ||
.receive(on: environment.mainQueue) | ||
.catchToEffect(SearchAction.citiesResponse) | ||
.cancellable(id: SearchCityId(), cancelInFlight: true) | ||
@Dependency(\.weatherClient) var weatherClient | ||
|
||
case .citiesResponse(let result): | ||
switch result { | ||
case .success(let list): | ||
state.status = list.count > 0 ? .normal : .noResult | ||
state.list = list | ||
case .failure(let error): | ||
state.status = .failed(error.localizedDescription) | ||
state.list = [] | ||
var body: some ReducerProtocol<State, Action> { | ||
BindingReducer() | ||
Reduce { state, action in | ||
switch action { | ||
case .binding(let action): | ||
if action.keyPath == \.$searchQuery, state.searchQuery.count == 0 { | ||
state.status = .normal | ||
state.list = [] | ||
} | ||
return .none | ||
|
||
case .search(let query): | ||
struct SearchCityId: Hashable { } | ||
|
||
guard state.status != .loading else { return .none } | ||
state.status = .loading | ||
return .task { | ||
await .citiesResponse(TaskResult<[Find.City]> { | ||
try await weatherClient.searchCity(query) | ||
}) | ||
} | ||
|
||
case .citiesResponse(let result): | ||
switch result { | ||
case .success(let list): | ||
state.status = list.count > 0 ? .normal : .noResult | ||
state.list = list | ||
case .failure(let error): | ||
state.status = .failed(error.localizedDescription) | ||
state.list = [] | ||
} | ||
return .none | ||
} | ||
} | ||
return .none | ||
} | ||
} | ||
.binding() | ||
.debug() |
Oops, something went wrong.