I am attempting to add .refreshable to my SwiftUI openweathermap app, in order to pull down and refresh values being returned to the app from an API. I set up my app to allow the user to enter a city name in the text field, hit a search button, and view weather details for that city in a sheet. After closing the sheet, the user can see all of his/her previously searched cities as navigation links in a list, with the city name and temperature visible in each list link. I attempted to add .refreshable {}
to the List in my ContentView. I tried setting up .refreshable to call fetchWeather()
in my ViewModel, which in turn is set up to pass the user-inputted cityName as a parameter into the API URL (also in the ViewModel). However, I'm now thinking this won't work to refresh the weather data, as the action for calling fetchWeather()
is defined in the toolbar button and not in the list. Any idea how I can set up .refreshable to refresh the weather data for each of the searched cities in the list? See my code below. Thanks!
ContentView
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
@StateObject var viewModel = WeatherViewModel()
@State private var cityName = ""
@State private var showingDetail = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city), label: {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°").font(.system(size: 32))
})
}.onDelete { index in
self.viewModel.cityNameList.remove(atOffsets: index)
}
}.refreshable {
viewModel.fetchWeather(for: cityName)
}
}.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: (.bottomBar)) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DetailView
struct DetailView: View {
@StateObject var viewModel = WeatherViewModel()
@State private var cityName = ""
@State var selection: Int? = nil
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
@Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}