0

I am trying to build an app using SwiftUI, and I've created a class responsible for performing an API request and decoding the response into a weatherResponse object, and have annotated this object as @Published within this class.

In my view, I have instantiated this class using the @StateObject annotation. According to this post, this should be the correct way of observing the weatherResponse object within my view so that it automatically updates.

However, when my view loads the instance of WeatherNetworking hasn't even been initialized, and so the first reference to the object published within that class results in a nil value access.

struct MainWeatherView: View {
    
    @StateObject var weatherNetworking = WeatherNetworking()

    var body: some View {
        VStack {
            Image(systemName: weatherNetworking.getConditionName(weatherID: (weatherNetworking.weatherResponse!.current.weather[0].id))) // weatherResponse is nil
                .resizable()
                .aspectRatio( contentMode: .fit)
                .scaleEffect(0.75)
                .padding()
            
            HStack {
                
                VStack{
                    Text("Temperature")
                        .fontWeight(.bold)
                        .font(.system(size: 24))
                    Text("Humidity")
                        .fontWeight(.bold)
                        .font(.system(size: 24))
                    ...
                }
                
                VStack {
                    Text("\(weatherNetworking.weatherResponse!.current.temp, specifier: "%.2f") °F")
                        .fontWeight(.bold)
                        .font(.system(size: 24))
                    Text("\(weatherNetworking.weatherResponse!.current.humidity, specifier: "%.0f") %")
                        .fontWeight(.bold)
                        .font(.system(size: 24))
                    ...
                }
            }
        }
        .onAppear {
            self.weatherNetworking.getMainWeather()
        }
    }
}
class WeatherNetworking: ObservableObject {
    
    @StateObject var locationManager = LocationManager()
    @Published var weatherResponse: WeatherResponse?
    
    func getMainWeather() {
        print("Location:", locationManager.lastLocation?.coordinate.latitude, locationManager.lastLocation?.coordinate.longitude)
        if let loc = URL(string: "https://api.openweathermap.org/data/2.5/onecall?appid=redacted&exclude=minutely&units=imperial&lat=\(locationManager.lastLocation?.coordinate.latitude ?? 0)&lon=\(locationManager.lastLocation?.coordinate.longitude ?? 0)") {
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: loc) { data, response, error in
                if error == nil {
                    let decoder = JSONDecoder()
                    if let safeData = data {
                        do {
                            let results = try decoder.decode(WeatherResponse.self, from: safeData)
                            DispatchQueue.main.async {
                                self.weatherResponse = results
                            }
                            print("weatherResponse was succesfully updated")
                        } catch {
                            print(error)
                        }
                    }
                } else {
                    print(error!)
                }
            }
            task.resume()
        }
    } 
}

None of the print statements within getMainWeather() execute which leads me to believe that the view is attempting to assign values before the function is called. How can I delay the assignment of these values within my view until after the onAppear() method finishes? it is worth noting that just as MainWeatherView depends upon the instantiation and asynchronous calls in WeatherNetworking, so too WeatherNetworking depends upon an instance of LocationManager.


class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    
    private let locationManager = CLLocationManager()
    @Published var locationStatus: CLAuthorizationStatus?
    @Published var lastLocation: CLLocation?

    ...
}
Cristik
  • 30,989
  • 25
  • 91
  • 127
John Harrington
  • 1,314
  • 12
  • 36

1 Answers1

3

Make dependent views conditional, like

VStack {
   if let response = weatherNetworking.weatherResponse {
        Image(systemName: weatherNetworking.getConditionName(weatherID: (response.current.weather[0].id))) // weatherResponse is nil
            .resizable()
            .aspectRatio( contentMode: .fit)
            .scaleEffect(0.75)
            .padding()
   }
   // .. other code
Asperi
  • 228,894
  • 20
  • 464
  • 690