@@ -10,17 +10,30 @@ private let readMe = """
1010// MARK: - Search feature domain
1111
1212struct SearchState : Equatable {
13- var locations : [ Location ] = [ ]
14- var locationWeather : LocationWeather ?
15- var locationWeatherRequestInFlight : Location ?
13+ var results : [ Search . Result ] = [ ]
14+ var resultForecastRequestInFlight : Search . Result ?
1615 var searchQuery = " "
16+ var weather : Weather ?
17+
18+ struct Weather : Equatable {
19+ var id : Search . Result . ID
20+ var days : [ Day ]
21+
22+ struct Day : Equatable {
23+ var date : Date
24+ var temperatureMax : Double
25+ var temperatureMaxUnit : String
26+ var temperatureMin : Double
27+ var temperatureMinUnit : String
28+ }
29+ }
1730}
1831
1932enum SearchAction : Equatable {
20- case locationsResponse( Result < [ Location ] , WeatherClient . Failure > )
21- case locationTapped( Location )
22- case locationWeatherResponse( Result < LocationWeather , WeatherClient . Failure > )
33+ case forecastResponse( Search . Result . ID , Result < Forecast , WeatherClient . Failure > )
2334 case searchQueryChanged( String )
35+ case searchResponse( Result < Search , WeatherClient . Failure > )
36+ case searchResultTapped( Search . Result )
2437}
2538
2639struct SearchEnvironment {
@@ -33,25 +46,27 @@ struct SearchEnvironment {
3346let searchReducer = Reducer < SearchState , SearchAction , SearchEnvironment > {
3447 state, action, environment in
3548 switch action {
36- case . locationsResponse( . failure) :
37- state. locations = [ ]
49+ case . forecastResponse( _, . failure) :
50+ state. weather = nil
51+ state. resultForecastRequestInFlight = nil
3852 return . none
3953
40- case let . locationsResponse( . success( response) ) :
41- state. locations = response
54+ case let . forecastResponse( id, . success( forecast) ) :
55+ state. weather = . init(
56+ id: id,
57+ days: forecast. daily. time. indices. map {
58+ . init(
59+ date: forecast. daily. time [ $0] ,
60+ temperatureMax: forecast. daily. temperatureMax [ $0] ,
61+ temperatureMaxUnit: forecast. dailyUnits. temperatureMax,
62+ temperatureMin: forecast. daily. temperatureMin [ $0] ,
63+ temperatureMinUnit: forecast. dailyUnits. temperatureMin
64+ )
65+ }
66+ )
67+ state. resultForecastRequestInFlight = nil
4268 return . none
4369
44- case let . locationTapped( location) :
45- enum SearchWeatherId { }
46-
47- state. locationWeatherRequestInFlight = location
48-
49- return environment. weatherClient
50- . weather ( location. id)
51- . receive ( on: environment. mainQueue)
52- . catchToEffect ( SearchAction . locationWeatherResponse)
53- . cancellable ( id: SearchWeatherId . self, cancelInFlight: true )
54-
5570 case let . searchQueryChanged( query) :
5671 enum SearchLocationId { }
5772
@@ -60,25 +75,35 @@ let searchReducer = Reducer<SearchState, SearchAction, SearchEnvironment> {
6075 // When the query is cleared we can clear the search results, but we have to make sure to cancel
6176 // any in-flight search requests too, otherwise we may get data coming in later.
6277 guard !query. isEmpty else {
63- state. locations = [ ]
64- state. locationWeather = nil
78+ state. results = [ ]
79+ state. weather = nil
6580 return . cancel( id: SearchLocationId . self)
6681 }
6782
6883 return environment. weatherClient
69- . searchLocation ( query)
84+ . search ( query)
7085 . debounce ( id: SearchLocationId . self, for: 0.3 , scheduler: environment. mainQueue)
71- . catchToEffect ( SearchAction . locationsResponse )
86+ . catchToEffect ( SearchAction . searchResponse )
7287
73- case let . locationWeatherResponse( . failure( locationWeather) ) :
74- state. locationWeather = nil
75- state. locationWeatherRequestInFlight = nil
88+ case . searchResponse( . failure) :
89+ state. results = [ ]
7690 return . none
7791
78- case let . locationWeatherResponse( . success( locationWeather) ) :
79- state. locationWeather = locationWeather
80- state. locationWeatherRequestInFlight = nil
92+ case let . searchResponse( . success( response) ) :
93+ state. results = response. results
8194 return . none
95+
96+ case let . searchResultTapped( location) :
97+ enum SearchWeatherId { }
98+
99+ state. resultForecastRequestInFlight = location
100+
101+ return environment. weatherClient
102+ . forecast ( location)
103+ . receive ( on: environment. mainQueue)
104+ . catchToEffect ( )
105+ . map { . forecastResponse( location. id, $0) }
106+ . cancellable ( id: SearchWeatherId . self, cancelInFlight: true )
82107 }
83108}
84109
@@ -109,27 +134,27 @@ struct SearchView: View {
109134 . padding ( . horizontal, 16 )
110135
111136 List {
112- ForEach ( viewStore. locations , id : \ . id ) { location in
137+ ForEach ( viewStore. results ) { location in
113138 VStack ( alignment: . leading) {
114- Button ( action: { viewStore. send ( . locationTapped ( location) ) } ) {
139+ Button ( action: { viewStore. send ( . searchResultTapped ( location) ) } ) {
115140 HStack {
116- Text ( location. title )
141+ Text ( location. name )
117142
118- if viewStore. locationWeatherRequestInFlight ? . id == location. id {
143+ if viewStore. resultForecastRequestInFlight ? . id == location. id {
119144 ProgressView ( )
120145 }
121146 }
122147 }
123148
124- if location. id == viewStore. locationWeather ? . id {
125- self . weatherView ( locationWeather: viewStore. locationWeather )
149+ if location. id == viewStore. weather ? . id {
150+ self . weatherView ( locationWeather: viewStore. weather )
126151 }
127152 }
128153 }
129154 }
130155
131- Button ( " Weather API provided by MetaWeather.com " ) {
132- UIApplication . shared. open ( URL ( string: " http ://www.MetaWeather. com" ) !)
156+ Button ( " Weather API provided by Open-Meteo " ) {
157+ UIApplication . shared. open ( URL ( string: " https ://open-meteo. com/en " ) !)
133158 }
134159 . foregroundColor ( . gray)
135160 . padding ( . all, 16 )
@@ -140,12 +165,12 @@ struct SearchView: View {
140165 }
141166 }
142167
143- func weatherView( locationWeather: LocationWeather ? ) -> some View {
168+ func weatherView( locationWeather: SearchState . Weather ? ) -> some View {
144169 guard let locationWeather = locationWeather else {
145170 return AnyView ( EmptyView ( ) )
146171 }
147172
148- let days = locationWeather. consolidatedWeather
173+ let days = locationWeather. days
149174 . enumerated ( )
150175 . map { idx, weather in formattedWeatherDay ( weather, isToday: idx == 0 ) }
151176
@@ -162,21 +187,17 @@ struct SearchView: View {
162187
163188// MARK: - Private helpers
164189
165- private func formattedWeatherDay( _ data : LocationWeather . ConsolidatedWeather , isToday: Bool )
190+ private func formattedWeatherDay( _ day : SearchState . Weather . Day , isToday: Bool )
166191 -> String
167192{
168193 let date =
169194 isToday
170195 ? " Today "
171- : dateFormatter. string ( from: data. applicableDate) . capitalized
172-
173- return [
174- date,
175- " \( Int ( round ( data. theTemp) ) ) ℃ " ,
176- data. weatherStateName,
177- ]
178- . compactMap { $0 }
179- . joined ( separator: " , " )
196+ : dateFormatter. string ( from: day. date) . capitalized
197+ let min = " \( day. temperatureMin) \( day. temperatureMinUnit) "
198+ let max = " \( day. temperatureMax) \( day. temperatureMaxUnit) "
199+
200+ return " \( date) , \( min) – \( max) "
180201}
181202
182203private let dateFormatter : DateFormatter = {
@@ -194,43 +215,8 @@ struct SearchView_Previews: PreviewProvider {
194215 reducer: searchReducer,
195216 environment: SearchEnvironment (
196217 weatherClient: WeatherClient (
197- searchLocation: { _ in
198- Effect ( value: [
199- Location ( id: 1 , title: " Brooklyn " ) ,
200- Location ( id: 2 , title: " Los Angeles " ) ,
201- Location ( id: 3 , title: " San Francisco " ) ,
202- ] )
203- } ,
204- weather: { id in
205- Effect (
206- value: LocationWeather (
207- consolidatedWeather: [
208- . init(
209- applicableDate: Date ( timeIntervalSince1970: 0 ) ,
210- maxTemp: 90 ,
211- minTemp: 70 ,
212- theTemp: 80 ,
213- weatherStateName: " Clear "
214- ) ,
215- . init(
216- applicableDate: Date ( timeIntervalSince1970: 86_400 ) ,
217- maxTemp: 70 ,
218- minTemp: 50 ,
219- theTemp: 60 ,
220- weatherStateName: " Rain "
221- ) ,
222- . init(
223- applicableDate: Date ( timeIntervalSince1970: 172_800 ) ,
224- maxTemp: 100 ,
225- minTemp: 80 ,
226- theTemp: 90 ,
227- weatherStateName: " Cloudy "
228- ) ,
229- ] ,
230- id: id
231- )
232- )
233- }
218+ forecast: { _ in Effect ( value: . mock) } ,
219+ search: { _ in Effect ( value: . mock) }
234220 ) ,
235221 mainQueue: . main
236222 )
0 commit comments