View only updated by EnvironmentObject after App-restart

Viewed 171

I have this weird behaviour, which I have been trying to fix for many hours:

One of my views is not being updated when the EnvironmentObject UserSettings changes. Although if I restart the App completely, the new value is used as it should.

Here is a short gif: https://s8.gifyu.com/images/Screen-Recording-2020-11-17-at-15.36.07.gif

So the value is stored properly and the View is accessing the right instance but since it is @Published, I would expect it to change instantly like in the other view. Any idea what Im doing wrong?

I built a small App that shows the same behaviour:

UserSettings.swift

import SwiftUI

final class UserSettings: ObservableObject {
    
    @Published var colorSelection: Int {
        didSet {
            UserDefaults.standard.set(colorSelection, forKey: "colorSelection")
        }
    }
    
    @Published var qnh: Double {
        didSet {
            UserDefaults.standard.set(qnh, forKey: "qnh")
        }
    }
    
    @Published var offset: Double {
        didSet {
            UserDefaults.standard.set(offset, forKey: "offset")
        }
    }
    
    @Published var colors: Array<Color> {
        didSet {
            UserDefaults.standard.set(colors, forKey: "colors")
        }
    }
    
    init() {
        self.colorSelection = UserDefaults.standard.object(forKey: "colorSelection") as? Int ?? 0
        self.qnh = UserDefaults.standard.object(forKey: "qnh") as? Double ?? 1013.25
        self.offset = UserDefaults.standard.object(forKey: "offset") as? Double ?? 0
        self.colors = UserDefaults.standard.object(forKey: "colors") as? Array ?? [Color.green, Color.white, Color.red, Color.blue, Color.orange, Color.yellow, Color.pink, Color.purple, Color.black]
    }
}

TestApp.swift

import SwiftUI

@main
struct TestApp: App {
    @StateObject var userSettings = UserSettings()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(UserSettings())
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var userSettings: UserSettings
    @State var view = 2
    
    var body: some View {
        TabView(selection: $view) {
            MainView()
                .environmentObject(UserSettings())
                .tabItem {Image(systemName: "location")}
                .tag(0)
            NavigationView {
                SettingsView()
                    .environmentObject(UserSettings())
                    .navigationTitle("Settings")
            }
            .tabItem {Image(systemName: "gearshape")}
            .tag(2)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(UserSettings())
    }
}

MainView.swift

import SwiftUI

struct MainView: View {
    @EnvironmentObject var userSettings: UserSettings
    
    var body: some View {
       Text("Test")
        .foregroundColor(userSettings.colors[userSettings.colorSelection])
    }
}

SettingsView.swift

import SwiftUI

struct SettingsView: View {
    @EnvironmentObject var userSettings: UserSettings
    @State private var selection: Int = 0
    private let colors = ["Green", "White", "Red", "Blue", "Orange", "Yellow", "Pink", "Purple", "Black"]
    
    var body: some View {
        Form {
            Section(header: Text("Customize")) {
                Picker(selection: $userSettings.colorSelection, label: Text("Font Color")) {
                    ForEach(0 ..< colors.count) {
                        Text(self.colors[$0]).foregroundColor(userSettings.colors[$0])
                    }.foregroundColor(userSettings.colors[userSettings.colorSelection])
                }
            }
        }
    }
}
2 Answers

You are creating new objects of UserSetting and passing them to the subview. You should create that object ones and pass it to the subviews.

In your ContentView

struct ContentView: View {
    @EnvironmentObject var userSettings: UserSettings
    @State var view = 2
    
    var body: some View {
        TabView(selection: $view) {
            MainView()
                .environmentObject(userSetting) //<< here pass the correct one
                .tabItem {Image(systemName: "location")}
                .tag(0)
            NavigationView {
                SettingsView()
                    .environmentObject(userSetting) //<< here aswell
                    .navigationTitle("Settings")
            }
            .tabItem {Image(systemName: "gearshape")}
            .tag(2)
        }
    }
}

Edit:

And its actually even simpler as you do not have to pass that manually to the subview. Just declare

@EnvironmentObject var userSettings: UserSettings

in your views and you can access it, without passing it.

If you call UserSettings() it will create a new instance of your class.

You need to pass the correct UserSettings instance (instead of creating a new one):

MainView.swift

@main
struct TestApp: App {
    @StateObject var userSettings = UserSettings()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(userSettings) // pass here
        }
    }
}

ContentView.swift

MainView()
    .environmentObject(userSettings) // pass here
    .tabItem { Image(systemName: "location") }
    .tag(0)
NavigationView {
    SettingsView()
        .environmentObject(userSettings) // pass here
        .navigationTitle("Settings")
}
Related