@@ -11,17 +11,30 @@ private let readMe = """
11
11
// MARK: - Search feature domain
12
12
13
13
struct SearchState : Equatable {
14
- var locations : [ Location ] = [ ]
15
- var locationWeather : LocationWeather ?
16
- var locationWeatherRequestInFlight : Location ?
14
+ var results : [ Search . Result ] = [ ]
15
+ var resultForecastRequestInFlight : Search . Result ?
17
16
var searchQuery = " "
17
+ var weather : Weather ?
18
+
19
+ struct Weather : Equatable {
20
+ var id : Search . Result . ID
21
+ var days : [ Day ]
22
+
23
+ struct Day : Equatable {
24
+ var date : Date
25
+ var temperatureMax : Double
26
+ var temperatureMaxUnit : String
27
+ var temperatureMin : Double
28
+ var temperatureMinUnit : String
29
+ }
30
+ }
18
31
}
19
32
20
33
enum SearchAction : Equatable {
21
- case locationsResponse( Result < [ Location ] , WeatherClient . Failure > )
22
- case locationTapped( Location )
23
- case locationWeatherResponse( Result < LocationWeather , WeatherClient . Failure > )
34
+ case forecastResponse( Search . Result . ID , Result < Forecast , WeatherClient . Failure > )
24
35
case searchQueryChanged( String )
36
+ case searchResponse( Result < Search , WeatherClient . Failure > )
37
+ case searchResultTapped( Search . Result )
25
38
}
26
39
27
40
struct SearchEnvironment {
@@ -34,25 +47,27 @@ struct SearchEnvironment {
34
47
let searchReducer = Reducer < SearchState , SearchAction , SearchEnvironment > {
35
48
state, action, environment in
36
49
switch action {
37
- case . locationsResponse( . failure) :
38
- state. locations = [ ]
50
+ case . forecastResponse( _, . failure) :
51
+ state. weather = nil
52
+ state. resultForecastRequestInFlight = nil
39
53
return . none
40
54
41
- case let . locationsResponse( . success( response) ) :
42
- state. locations = response
55
+ case let . forecastResponse( id, . success( forecast) ) :
56
+ state. weather = . init(
57
+ id: id,
58
+ days: forecast. daily. time. indices. map {
59
+ . init(
60
+ date: forecast. daily. time [ $0] ,
61
+ temperatureMax: forecast. daily. temperatureMax [ $0] ,
62
+ temperatureMaxUnit: forecast. dailyUnits. temperatureMax,
63
+ temperatureMin: forecast. daily. temperatureMin [ $0] ,
64
+ temperatureMinUnit: forecast. dailyUnits. temperatureMin
65
+ )
66
+ }
67
+ )
68
+ state. resultForecastRequestInFlight = nil
43
69
return . none
44
70
45
- case let . locationTapped( location) :
46
- enum SearchWeatherId { }
47
-
48
- state. locationWeatherRequestInFlight = location
49
-
50
- return environment. weatherClient
51
- . weather ( location. id)
52
- . observe ( on: environment. mainQueue)
53
- . catchToEffect ( SearchAction . locationWeatherResponse)
54
- . cancellable ( id: SearchWeatherId . self, cancelInFlight: true )
55
-
56
71
case let . searchQueryChanged( query) :
57
72
enum SearchLocationId { }
58
73
@@ -61,25 +76,35 @@ let searchReducer = Reducer<SearchState, SearchAction, SearchEnvironment> {
61
76
// When the query is cleared we can clear the search results, but we have to make sure to cancel
62
77
// any in-flight search requests too, otherwise we may get data coming in later.
63
78
guard !query. isEmpty else {
64
- state. locations = [ ]
65
- state. locationWeather = nil
79
+ state. results = [ ]
80
+ state. weather = nil
66
81
return . cancel( id: SearchLocationId . self)
67
82
}
68
83
69
84
return environment. weatherClient
70
- . searchLocation ( query)
85
+ . search ( query)
71
86
. debounce ( id: SearchLocationId . self, for: 0.3 , scheduler: environment. mainQueue)
72
- . catchToEffect ( SearchAction . locationsResponse )
87
+ . catchToEffect ( SearchAction . searchResponse )
73
88
74
- case let . locationWeatherResponse( . failure( locationWeather) ) :
75
- state. locationWeather = nil
76
- state. locationWeatherRequestInFlight = nil
89
+ case . searchResponse( . failure) :
90
+ state. results = [ ]
77
91
return . none
78
92
79
- case let . locationWeatherResponse( . success( locationWeather) ) :
80
- state. locationWeather = locationWeather
81
- state. locationWeatherRequestInFlight = nil
93
+ case let . searchResponse( . success( response) ) :
94
+ state. results = response. results
82
95
return . none
96
+
97
+ case let . searchResultTapped( location) :
98
+ enum SearchWeatherId { }
99
+
100
+ state. resultForecastRequestInFlight = location
101
+
102
+ return environment. weatherClient
103
+ . forecast ( location)
104
+ . observe ( on: environment. mainQueue)
105
+ . catchToEffect ( )
106
+ . map { . forecastResponse( location. id, $0) }
107
+ . cancellable ( id: SearchWeatherId . self, cancelInFlight: true )
83
108
}
84
109
}
85
110
@@ -110,27 +135,27 @@ struct SearchView: View {
110
135
. padding ( . horizontal, 16 )
111
136
112
137
List {
113
- ForEach ( viewStore. locations , id : \ . id ) { location in
138
+ ForEach ( viewStore. results ) { location in
114
139
VStack ( alignment: . leading) {
115
- Button ( action: { viewStore. send ( . locationTapped ( location) ) } ) {
140
+ Button ( action: { viewStore. send ( . searchResultTapped ( location) ) } ) {
116
141
HStack {
117
- Text ( location. title )
142
+ Text ( location. name )
118
143
119
- if viewStore. locationWeatherRequestInFlight ? . id == location. id {
144
+ if viewStore. resultForecastRequestInFlight ? . id == location. id {
120
145
ProgressView ( )
121
146
}
122
147
}
123
148
}
124
149
125
- if location. id == viewStore. locationWeather ? . id {
126
- self . weatherView ( locationWeather: viewStore. locationWeather )
150
+ if location. id == viewStore. weather ? . id {
151
+ self . weatherView ( locationWeather: viewStore. weather )
127
152
}
128
153
}
129
154
}
130
155
}
131
156
132
- Button ( " Weather API provided by MetaWeather.com " ) {
133
- UIApplication . shared. open ( URL ( string: " http ://www.MetaWeather. com" ) !)
157
+ Button ( " Weather API provided by Open-Meteo " ) {
158
+ UIApplication . shared. open ( URL ( string: " https ://open-meteo. com/en " ) !)
134
159
}
135
160
. foregroundColor ( . gray)
136
161
. padding ( . all, 16 )
@@ -141,12 +166,12 @@ struct SearchView: View {
141
166
}
142
167
}
143
168
144
- func weatherView( locationWeather: LocationWeather ? ) -> some View {
169
+ func weatherView( locationWeather: SearchState . Weather ? ) -> some View {
145
170
guard let locationWeather = locationWeather else {
146
171
return AnyView ( EmptyView ( ) )
147
172
}
148
173
149
- let days = locationWeather. consolidatedWeather
174
+ let days = locationWeather. days
150
175
. enumerated ( )
151
176
. map { idx, weather in formattedWeatherDay ( weather, isToday: idx == 0 ) }
152
177
@@ -163,21 +188,17 @@ struct SearchView: View {
163
188
164
189
// MARK: - Private helpers
165
190
166
- private func formattedWeatherDay( _ data : LocationWeather . ConsolidatedWeather , isToday: Bool )
191
+ private func formattedWeatherDay( _ day : SearchState . Weather . Day , isToday: Bool )
167
192
-> String
168
193
{
169
194
let date =
170
195
isToday
171
196
? " Today "
172
- : dateFormatter. string ( from: data. applicableDate) . capitalized
173
-
174
- return [
175
- date,
176
- " \( Int ( round ( data. theTemp) ) ) ℃ " ,
177
- data. weatherStateName,
178
- ]
179
- . compactMap { $0 }
180
- . joined ( separator: " , " )
197
+ : dateFormatter. string ( from: day. date) . capitalized
198
+ let min = " \( day. temperatureMin) \( day. temperatureMinUnit) "
199
+ let max = " \( day. temperatureMax) \( day. temperatureMaxUnit) "
200
+
201
+ return " \( date) , \( min) – \( max) "
181
202
}
182
203
183
204
private let dateFormatter : DateFormatter = {
@@ -195,43 +216,8 @@ struct SearchView_Previews: PreviewProvider {
195
216
reducer: searchReducer,
196
217
environment: SearchEnvironment (
197
218
weatherClient: WeatherClient (
198
- searchLocation: { _ in
199
- Effect ( value: [
200
- Location ( id: 1 , title: " Brooklyn " ) ,
201
- Location ( id: 2 , title: " Los Angeles " ) ,
202
- Location ( id: 3 , title: " San Francisco " ) ,
203
- ] )
204
- } ,
205
- weather: { id in
206
- Effect (
207
- value: LocationWeather (
208
- consolidatedWeather: [
209
- . init(
210
- applicableDate: Date ( timeIntervalSince1970: 0 ) ,
211
- maxTemp: 90 ,
212
- minTemp: 70 ,
213
- theTemp: 80 ,
214
- weatherStateName: " Clear "
215
- ) ,
216
- . init(
217
- applicableDate: Date ( timeIntervalSince1970: 86_400 ) ,
218
- maxTemp: 70 ,
219
- minTemp: 50 ,
220
- theTemp: 60 ,
221
- weatherStateName: " Rain "
222
- ) ,
223
- . init(
224
- applicableDate: Date ( timeIntervalSince1970: 172_800 ) ,
225
- maxTemp: 100 ,
226
- minTemp: 80 ,
227
- theTemp: 90 ,
228
- weatherStateName: " Cloudy "
229
- ) ,
230
- ] ,
231
- id: id
232
- )
233
- )
234
- }
219
+ forecast: { _ in Effect ( value: . mock) } ,
220
+ search: { _ in Effect ( value: . mock) }
235
221
) ,
236
222
mainQueue: QueueScheduler . main
237
223
)
0 commit comments