Learn Courses My Dashboard

Can't figure out to only get 12 hrs of hourly weather temp

Hi. I’m having a brain block right now and I can’t figure out how to display only 12 hours of hourly temperature rather than the 48 hours with the OpenWeatherMap API OneCall.

Here is my ViewModel

class WeatherModel: ObservableObject {
    
    //Declare published property wrapper, that when the the property value changes, we want to notifty anyone who is observing it
    @Published var weatherData: Weather?
    @AppStorage("cityName") var cityName = ""


    init(){
        getWeatherData(cityName)
    }
    //Init method gets run when a new instance of WeatherModel is created
    
    
    //MARK: - OpenWeatherMap API methods
    
    
    func getWeatherData(_ cityName: String){
        CLGeocoder().geocodeAddressString(cityName){(placemarks, error ) in
            if let error = error {
                print(error)
            }
            if let lat = placemarks?.first?.location?.coordinate.latitude,
                let lon = placemarks?.first?.location?.coordinate.longitude {
                //first is the first element of the collection
                let weatherUrlString = "https://api.openweathermap.org/data/2.5/onecall?lat=\(lat)&lon=\(lon)&exclude=minutely,daily,alerts&units=imperial&appid=\(Constants.apiKey)"
                
                let weatherUrl = URL(string: weatherUrlString)
                guard weatherUrl != nil else {
                    return
                }
                
                let request = URLRequest(url: weatherUrl!)
                
                //Create a URL session
                
                let session = URLSession.shared
                let dataTask = session.dataTask(with: request) { data, response, error in
                    guard error == nil else {
                        return
                    }
                    
                    do{
                        let decoder = JSONDecoder()
                        var result = try decoder.decode(Weather.self, from: data!)
                        //parsing the weather data into the constant, result
                        
                        //Add UUId's to the hourly weather objects. Use the variable Result since that is parsing the weather
                        for i in 0..<result.hourly.count {
                                result.hourly[i].id = UUID()
                        }
                        
                        
                        DispatchQueue.main.async {
                            self.weatherData = result
    
                        }
                        
                    }catch {
                        print(error)
                    }
                }
                dataTask.resume()
            }
           }
        
        
        
        
        
    }//func getWeatherData
    
}

Model

struct Weather: Decodable {
 
    var current: Current
    var hourly: [Current]
    
    //Hourly is an arrary of weather responses (i.e. Current). It parses the data because the arrary is similar to Current properties
    
}


struct Current: Decodable, Identifiable {
    var id: UUID?
    var dt: Double
    var temp: Double
    var feels_like: Double
    var weather: [WeatherInfo]
    
}


struct WeatherInfo: Decodable {
    var description: String
}

Right now this is just a rough view and will update the look of it but for now I’m putting it as a list

View

   List(model.weatherData?.hourly ?? [] ) {
                            hour in
                            Text("\(Constants.dtConversion(hour.dt)), \(Constants.tempToString(hour.temp))")

You just need to take the prefix of your array. Something like this:

    var hourlyNext12: [Current] {
        guard let hourly = model.weatherData?.hourly else {
            return []
        }
        
        return Array(hourly.prefix(12))
    }
    
    var body: some View {
        List(hourlyNext12) { hour in
            //I removed your Constants stuff since I don't have that code
            Text("\(hour.dt), \(hour.temp)")
        }
    }

You could also make this a method on your WeatherModel class and handle it all in there.

And a couple of suggestions:

  1. Change your Current struct to this:
struct Current: Decodable, Identifiable {
    var id: Date { dt }
    var dt: Date
    var temp: Double
    var feels_like: Double
    var weather: [WeatherInfo]
}

Then when you decode the API call results, use

decoder.dateDecodingStrategy = .secondsSince1970

to translate the timestamp given in the JSON to an actual Swift Date object. You can use the dt property as the id since they will be unique. And you won’t need to loop through all the decoded items and assign a UUID.

If you want to stick with an assigned UUID as the id, you don’t need to loop and assign. Just use let id = UUID() in the struct and a UUID will be automatically assigned when the struct is created.

  1. Store the latitude and longitude you get from CLGeocoder in AppSettings as well as the city name. Only call CLGeocoder().geocodeAddressString if the city being looked up hasn’t already been geocoded and stored. You could even store a Dictionary of cities and coordinates you have looked up in the past. It might not make a lot of difference in performance, but every millisecond counts for improving the user experience. And it will save you from having to geocode something you’ve already done before.

Thanks for the help! For the decoder.dateDecodingStrategy = .secondsSince1970 where would I format to the “h a” where it would display 5PM.
Here was the function I was utilizing for string interpolation

    static func dtConversion(_ unix: Double) -> String {
        
        let date = Date(timeIntervalSince1970: unix)
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "h a"
        
        
        return dateFormatter.string(from: date)
        
        //Allows us to pass in a date and return a string in the format we specify
    }

Would I need to make an instance of DateFormatter in the model, following the decoder.dateDecondingStrategy?

Sorry. I’m a bit confused the CLGeocoder in AppSettings portion (beginner here). Would the latitude and longitude be in the properties? So @AppStorage("latitude) var latitude = Double?

DateFormatter.dateFormat is for converting date strings to Date objects and for outputting fixed format dates (such as en_US_POSIX) that ignore the user’s locale preferences. For displaying dates to users in their localized formats, you should be using dateStyle and timeStyle or setLocalizedDateFormatFromTemplate(_:) because they will always display correctly for the user’s current locale. In this case, since the format you want isn’t covered by a built-in timeStyle (the closest would be .short, which in the en_US locale gives e.g. “8:00 AM”), you would use the function.

Here’s how I would do it:

struct Current: Decodable, Identifiable {
    let id = UUID()
    var dt: Date
    var temp: Double
    var feels_like: Double
    var weather: [WeatherInfo]
    
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.setLocalizedDateFormatFromTemplate("h a") // 8 AM
        return formatter
    }()
    
    var formattedDate: String {
        Current.dateFormatter.string(from: dt)
    }
}

And then in your Text, you would use hour.formattedDate instead of Constants.dtConversion(hour.dt).

Sorry. I’m a bit confused the CLGeocoder in AppSettings portion (beginner here). Would the latitude and longitude be in the properties? So @AppStorage("latitude) var latitude = Double?

Yes, that’s what I was getting at. Totally up to you, but my thinking was to cache those values so you don’t have to hit CLGeocoder every time you fetch weather for the same city.

func getWeather(_ cityName: String) {
    //compare the cityName parameter with the value we have in
    //@AppStorage("cityName")
    if cityName != self.cityName {
        //use CLGeocoder to get the latitude and longitude
        //and store them and the new cityName in your settings
        //using @AppStorage
    }

    //fetch weather data from the API using whatever latitude and longitude
    //values are in your settings
}

Thank you for the clarification and input! I appreciate it. Glad to learn something.