Skip to content

Commit

Permalink
Merge branch 'feature/tca-reducer-protocol' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Starkrimson committed Nov 2, 2022
2 parents 5c42047 + 7fa1623 commit 9043710
Show file tree
Hide file tree
Showing 14 changed files with 423 additions and 453 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ fastlane/test_output

iOSInjectionProject/

.idea/
.idea/
.DS_Store
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import Foundation

let testCities = try! JSONDecoder().decode([Find.City].self, from: testCitiesData)
let testCitiesData = """
[
{
"id": 1809858,
Expand Down Expand Up @@ -68,4 +72,5 @@
}
]
}
]
]
""".data(using: .utf8)!
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import Foundation

let testOneCall = try! JSONDecoder().decode(OneCall.self, from: testOneCallData)
let testOneCallData = """
{
"daily": [
{
Expand Down Expand Up @@ -1479,4 +1483,5 @@
"pop": 0.89000000000000001
}
]
}
}
""".data(using: .utf8)!
175 changes: 89 additions & 86 deletions Shared/State/DataFlow/ForecastState.swift
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()
102 changes: 51 additions & 51 deletions Shared/State/DataFlow/SearchState.swift
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()
Loading

0 comments on commit 9043710

Please sign in to comment.