Skip to content

Commit 1abeaaf

Browse files
mluisbrownmbrandonw
andcommitted
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 8706d02 commit 1abeaaf

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
@@ -11,17 +11,30 @@ private let readMe = """
1111
// MARK: - Search feature domain
1212

1313
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?
1716
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+
}
1831
}
1932

2033
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>)
2435
case searchQueryChanged(String)
36+
case searchResponse(Result<Search, WeatherClient.Failure>)
37+
case searchResultTapped(Search.Result)
2538
}
2639

2740
struct SearchEnvironment {
@@ -34,25 +47,27 @@ struct SearchEnvironment {
3447
let searchReducer = Reducer<SearchState, SearchAction, SearchEnvironment> {
3548
state, action, environment in
3649
switch action {
37-
case .locationsResponse(.failure):
38-
state.locations = []
50+
case .forecastResponse(_, .failure):
51+
state.weather = nil
52+
state.resultForecastRequestInFlight = nil
3953
return .none
4054

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
4369
return .none
4470

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-
5671
case let .searchQueryChanged(query):
5772
enum SearchLocationId {}
5873

@@ -61,25 +76,35 @@ let searchReducer = Reducer<SearchState, SearchAction, SearchEnvironment> {
6176
// When the query is cleared we can clear the search results, but we have to make sure to cancel
6277
// any in-flight search requests too, otherwise we may get data coming in later.
6378
guard !query.isEmpty else {
64-
state.locations = []
65-
state.locationWeather = nil
79+
state.results = []
80+
state.weather = nil
6681
return .cancel(id: SearchLocationId.self)
6782
}
6883

6984
return environment.weatherClient
70-
.searchLocation(query)
85+
.search(query)
7186
.debounce(id: SearchLocationId.self, for: 0.3, scheduler: environment.mainQueue)
72-
.catchToEffect(SearchAction.locationsResponse)
87+
.catchToEffect(SearchAction.searchResponse)
7388

74-
case let .locationWeatherResponse(.failure(locationWeather)):
75-
state.locationWeather = nil
76-
state.locationWeatherRequestInFlight = nil
89+
case .searchResponse(.failure):
90+
state.results = []
7791
return .none
7892

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
8295
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)
83108
}
84109
}
85110

@@ -110,27 +135,27 @@ struct SearchView: View {
110135
.padding(.horizontal, 16)
111136

112137
List {
113-
ForEach(viewStore.locations, id: \.id) { location in
138+
ForEach(viewStore.results) { location in
114139
VStack(alignment: .leading) {
115-
Button(action: { viewStore.send(.locationTapped(location)) }) {
140+
Button(action: { viewStore.send(.searchResultTapped(location)) }) {
116141
HStack {
117-
Text(location.title)
142+
Text(location.name)
118143

119-
if viewStore.locationWeatherRequestInFlight?.id == location.id {
144+
if viewStore.resultForecastRequestInFlight?.id == location.id {
120145
ProgressView()
121146
}
122147
}
123148
}
124149

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)
127152
}
128153
}
129154
}
130155
}
131156

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")!)
134159
}
135160
.foregroundColor(.gray)
136161
.padding(.all, 16)
@@ -141,12 +166,12 @@ struct SearchView: View {
141166
}
142167
}
143168

144-
func weatherView(locationWeather: LocationWeather?) -> some View {
169+
func weatherView(locationWeather: SearchState.Weather?) -> some View {
145170
guard let locationWeather = locationWeather else {
146171
return AnyView(EmptyView())
147172
}
148173

149-
let days = locationWeather.consolidatedWeather
174+
let days = locationWeather.days
150175
.enumerated()
151176
.map { idx, weather in formattedWeatherDay(weather, isToday: idx == 0) }
152177

@@ -163,21 +188,17 @@ struct SearchView: View {
163188

164189
// MARK: - Private helpers
165190

166-
private func formattedWeatherDay(_ data: LocationWeather.ConsolidatedWeather, isToday: Bool)
191+
private func formattedWeatherDay(_ day: SearchState.Weather.Day, isToday: Bool)
167192
-> String
168193
{
169194
let date =
170195
isToday
171196
? "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)"
181202
}
182203

183204
private let dateFormatter: DateFormatter = {
@@ -195,43 +216,8 @@ struct SearchView_Previews: PreviewProvider {
195216
reducer: searchReducer,
196217
environment: SearchEnvironment(
197218
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) }
235221
),
236222
mainQueue: QueueScheduler.main
237223
)

0 commit comments

Comments
 (0)