Skip to content

Commit 9704bc2

Browse files
Update Search Demo to use Open-Meteo (#1110)
MetaWeather.com has been offline for awhile now, with no sign of return. Open-Meteo appears to have a more active presence, so let's rely on it for now, instead. Co-authored-by: Brandon Williams <[email protected]>
1 parent c8a2e94 commit 9704bc2

File tree

3 files changed

+270
-199
lines changed

3 files changed

+270
-199
lines changed

Examples/Search/Search/SearchView.swift

Lines changed: 74 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,30 @@ private let readMe = """
1010
// MARK: - Search feature domain
1111

1212
struct 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

1932
enum 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

2639
struct SearchEnvironment {
@@ -33,25 +46,27 @@ struct SearchEnvironment {
3346
let 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

182203
private 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

Comments
 (0)