UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Environment Object not updating view

Forums > Swift

Hello folks, I have a problem with an environment object not properly updating content inside of a view.

I have an app that uses an object that defines a location via various properties (ie. name, latitude, longitude, description, etc.), I call this object "myArea". A list of these objects is presented in a listview displaying these locations read from a file stored in the app and the user can get the forecast for each location in the list by selecting one. The forecast is queried using the noaa's API which requires two calls to get the current forecast: the first where you submit the latitude and the longitude of the location in question and you get a URL back that to use to query for the forecast periods for that area (these URLs could potentially change so it is recommended that you don't store them). I call these responses "ForecastMetaData" and the last query is to that URL so that you can get the period forecast for the target location. I call that response object "PeriodForecastData". Both responses are in JSON format.

The problem that I am running into is as follows:

ContentView has an environment variable containing all of the list of locations loaded from the loca file. As each location is loaded I call a function "getForecastMetaData" that queries the web for the metadata for each area. ContentView displays all of these areas in a navigationview using a listview. The destination of each item in the list sends the user to another view called "DisplayForecast" passing the selected "myArea" object as an observed object to that view. When "DisplayForecast" loads I call a method of the passed in object that queries for the period forecast data, this mehtod is called "getForecastPeriods". The user is supposed to be presented with an activity indicator that says "Loading" until the query for the period forecast data is complete but even though that query completes, the acvitivty indicator never goes away. If I go back to the ContentView and then go back to that previously selected location then the indicator never shows up and the desired content shows up right away. It's as if the observed object that I am passing into the DisplayForecast view is not publishing the changes. Furthermore, to properly display the queried period data I use another view called "PeriodForecast" inside of "DisplayForecast" to which I pass the same ObservedObject that was passed to "DisplayForecast" but the App crashes since I am trying unwrap an optional (Periodforecast data) that for some reason is still not initialized. I suspect this all part of the same problem stemming from the ObservedObject not updating properly.

I had read this post that talked about using Combine and to let publisher know when an object had changed by using "objectWillChange.send()" and I tried to do that in my code below but it is still not working. Perhaps I am not doing this in the right place. The only work around that I have found so far is to call "getForecastPeriods" for each area right after "getForecastMetaData" completes succcessfully. But ideally I would like to query for periods forecast data as the user selects an area.

Here is my code:


// The information for how to build these JSON response models was obtained from: https://weather-gov.github.io/api/general-faqs#how-to-get-forecast
struct ForecastMetaData: Decodable {

    // map the key names for the properties array that contains the URLs we want
    enum CodingKeys: String, CodingKey {
        case properties = "properties"
    }

    struct ForecastProperties: Decodable {
        // map the key names for the forecast data that we are after
        enum CodingKeys: String, CodingKey {
            case forecastURL = "forecast"
            case forecastHourlyURL = "forecastHourly"
            case forecastGridDataURL = "forecastGridData"
        }

        let forecastURL: String
        let forecastHourlyURL: String
        let forecastGridDataURL: String
    }

    let properties: ForecastProperties
}

// This object is used to hold the JSON results of the query to the "forecast" URL
struct PeriodForecastData: Codable {
    struct ForecastPeriod: Codable, Identifiable {
        let id = UUID()
        let number: Int
        let name: String
        let isDaytime: Bool
        let temperature: Int
        let temperatureUnit: String
        let windSpeed: String
        let windDirection: String
        let icon: String
        let shortForecast: String
        let detailedForecast: String

        func getTempInfo() -> String {
            return String(temperature) + " " + temperatureUnit
        }

        func getWindInfo() -> String {
            return windSpeed + " " + windDirection
        }
    }

    struct ForecastProperties: Codable {
        let periods: [ForecastPeriod]
    }

    let properties: ForecastProperties
}

class AllAreas : ObservableObject, Decodable  {
    enum CodingKeys: String, CodingKey {
        case list = "areas"
    }

    @Published var list: [myArea]

    init() {
        if let areas = UserDefaults.standard.data(forKey: "ClimbingAreas") {
            let decoder = JSONDecoder()

            if let decoded = try? decoder.decode([myArea].self, from: areas) {
                self.list = decoded
                return
            }
        }
        self.list = myArea.getAreasData()
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        list = try container.decode([myArea].self, forKey: .list)
    }
}

class myArea: ObservableObject, Decodable, Identifiable, Hashable {
    enum CodingKeys: CodingKey {
        case id
        case name
        case bio
        case image
        case lat
        case lon
        case metadata
        case periodforecast
    }

    let id = UUID()
    let name: String
    let bio: String
    let image: String
    let lat: Double
    let lon: Double
    var metadata: ForecastMetaData?
    @Published var periodforecast: PeriodForecastData? 

    init(name: String, bio: String, category: Category, image: String, lat: Double, lon: Double) {
        self.name = name
        self.bio = bio
        self.image = image
        self.lat = lat
        self.lon = lon
        getForecastMetaData()
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        bio = try container.decode(String.self, forKey: .bio)
        image = try container.decode(String.self, forKey: .image)
        lat = try container.decode(Double.self, forKey: .lat)
        lon = try container.decode(Double.self, forKey: .lon)
        getForecastMetaData()
    }

    func getForecastMetaData() -> Void {
        JSONServices().getJSONURLResponse(urlString: "https://api.weather.gov/points/\(self.lat),\(self.lon)") { (metadata: ForecastMetaData?) in
            if let metadata = metadata {
                self.metadata = metadata
                // self.getForecastPeriods() // This works but it is not ideal
            }
        }
    }

    func getForecastPeriods() -> Void {
        if let metadata = self.metadata?.properties {
            JSONServices().getJSONURLResponse(urlString: metadata.forecastURL) { (result: PeriodForecastData?) in
                if let result = result {
                    self.periodforecast = result
                }
            }
        }
    }

    static func getAreasData() -> [myArea] {
        var allareas: [myArea] = []
        JSONServices().getJSONFileResponse(filename: "areas") { (collection: AllAreas?) in
            guard let collection = collection else {
                return
            }
            // sort the areas by name
            allareas = collection.list.sorted{ $0.name.compare($1.name,options: .caseInsensitive) == .orderedAscending }
        }
        //return areas
        return allareas
    }

    static func ==(lhs: myArea, rhs: myArea) -> Bool {
        return lhs.id == rhs.id && lhs.name == rhs.name
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

//
//  ContentView.swift
//  

import SwiftUI

struct ContentView: View {

    // get the information from the environment object defined in scene
    @EnvironmentObject var myAreas: AllAreas

    var body: some View {
        NavigationView {
                List() {
                ForEach(myAreas.list) { item in
                    ZStack {
                        ListItem().environmentObject(item) // This works fine and displays the item information as expected
                        NavigationLink(destination: DisplayForecast(Area: item)) {
                            EmptyView()
                        } .hidden().frame(width: 0) // hide the disclosure indicator ">"
                    }
                }
            }
            .navigationBarTitle(Text("my Areas"),displayMode: .large)
            .padding(.horizontal,-14) // remove the padding that the list adds to the screen
        }
    }
}

//
//  ActivityIndicator.swift
//  

import SwiftUI

struct ActivityIndicator: UIViewRepresentable {

    @Binding var isAnimating: Bool
    let style: UIActivityIndicatorView.Style

    func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
        return UIActivityIndicatorView(style: style)
    }

    func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
    }
}

struct LoadingView<Content>: View where Content: View {
    var isShowing: Bool
    var content: () -> Content
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {
                // load the content passed to the loadingview and the activity indicator on top of it
                self.content()
                    .disabled(self.isShowing)
                    .blur(radius: self.isShowing ? 3 : 0)
                HStack{
                    Spacer()
                    VStack {
                        Text("Loading...")
                        ActivityIndicator(isAnimating: .constant(true), style: .large)
                    }
                        .frame(width: geometry.size.width / 2,
                               height: geometry.size.height / 5)
                        .background(Color.secondary.colorInvert())
                        .foregroundColor(Color.primary)
                        .cornerRadius(20)
                        .opacity(self.isShowing ? 1 : 0)
                    Spacer()
                }
            }
        }
    }
}

//
//  DisplayForecast.swift
//  

import SwiftUI

struct DisplayForecast: View {
    @ObservedObject var Area: myArea
    //var areaName: String

    var body: some View {
        LoadingView(isShowing: (Area.periodforecast?.properties.periods.count ?? 0 <= 1)) { // show the loader only if we do not have results yet
            PeriodForecast(Area: Area)
            // Text("Location name is: \(Area.name)")
        }
        .onAppear() {
            //self.Area.objectWillChange.send() // Adding this DOES NOT WORK
            self.Area.getForecastPeriods()
        }
    }
}

//
//  PeriodForecast.swift
//  
//

import SwiftUI

struct PeriodForecast: View {
    @ObservedObject var Area: myArea
    var body: some View {
        ScrollView {
            VStack () {
                ForEach((self.Area.periodforecast?.properties.periods)!) { period in  // THIS CRASHES
                    if (period.number == 1) {
                        CurrentForecast(location: self.Area.name, imageURL: period.icon, temp: period.getTempInfo(), windspeed: period.getWindInfo(), periodname: period.name, details: period.detailedForecast)
                    } else {
                        Divider()
                        HStack{
                            ImageUrlView(url: period.icon,width: 80,height: 80)
                                .cornerRadius(5)
                            VStack (alignment: .leading) {
                                Text("\(period.name): \(period.getTempInfo())")
                                Text(period.detailedForecast)
                                    .fixedSize(horizontal: false, vertical: true) 
                            }
                            .font(.caption)
                            Spacer()
                        }
                            .padding(.horizontal,15)
                    }
                }
            }
        }
    }
}

3      

@broncoscoder did you ever find a solution to this? I am havng a similar problem with EnvironmentObject not updating the view when it is changed by the use of a function.

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.