How to access a global environment object in a class? - swiftui

I have a class that needs to update a global environment object. I can pass that environment object between my structs all day, but how do I allow a class object to access the same variable?
import SwiftUI
class Global: ObservableObject
{
#Published var num = 10
}
class MyClass:ObservableObject
{
#Published var mode = 1
#EnvironmentObject var global: Global
func updateMode()
{
self.mode += 1
global.num += 1
}
}
#main
struct MyApp: App
{
let settings = Global()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}
struct ContentView: View
{
#EnvironmentObject var global: Global
#ObservedObject var myClass = MyClass()
var body: some View
{
VStack
{
Text("Setting \(global.num)")
Text("Mode \(myClass.mode)")
Button("Click Me", action: {myClass.updateMode()})
}
.padding()
}
}
The following code gives an error:
Fatal Error: No ObservableObject of type Global found. A
View.environmentObject(_:) for Global maybe missing an ancestor of
this view.
I could pass the global object into myClass.updateMode, but then it doesn't seem very global at that point? I would have thought there must be a better way.

A possible approach is to make it shared (and don't use #EnvironmentObject anywhere outside SwiftUI view - it is not designed for that):
class Global: ObservableObject
{
static let shared = Global()
#Published var num = 10
}
class MyClass:ObservableObject
{
#Published var mode = 1
let global = Global.shared // << here !!
// ...
}
#main
struct MyApp: App
{
#StateObject var settings = Global.shared // << same !!
// ...
}

Related

How to implement singleton #ObservedObject in SwiftUI [duplicate]

I want to create a global variable for showing a loadingView, I tried lots of different ways but could not figure out how to. I need to be able to access this variable across the entire application and update the MotherView file when I change the boolean for the singleton.
struct MotherView: View {
#StateObject var viewRouter = ViewRouter()
var body: some View {
if isLoading { //isLoading needs to be on a singleton instance
Loading()
}
switch viewRouter.currentPage {
case .page1:
ContentView()
case .page2:
PostList()
}
}
}
struct MotherView_Previews: PreviewProvider {
static var previews: some View {
MotherView(viewRouter: ViewRouter())
}
}
I have tried the below singleton but it does not let me update the shared instance? How do I update a singleton instance?
struct LoadingSingleton {
static let shared = LoadingSingleton()
var isLoading = false
private init() { }
}
Make your singleton a ObservableObject with #Published properties:
struct ContentView: View {
#StateObject var loading = LoadingSingleton.shared
var body: some View {
if loading.isLoading {
Text("Loading...")
}
ChildView()
Button(action: { loading.isLoading.toggle() }) {
Text("Toggle loading")
}
}
}
struct ChildView : View {
#StateObject var loading = LoadingSingleton.shared
var body: some View {
if loading.isLoading {
Text("Child is loading")
}
}
}
class LoadingSingleton : ObservableObject {
static let shared = LoadingSingleton()
#Published var isLoading = false
private init() { }
}
I should mention that in SwiftUI, it's common to use .environmentObject to pass a dependency through the view hierarchy rather than using a singleton -- it might be worth looking into.
First, make LoadingSingleton a class that adheres to the ObservableObject protocol. Use the #Published property wrapper on isLoading so that your SwiftUI views update when it's changed.
class LoadingSingleton: ObservableObject {
#Published var isLoading = false
}
Then, put LoadingSingleton in your SceneDelegate and hook it into your SwiftUI views via environmentObject():
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
static let singleton = LoadingSingleton()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(SceneDelegate.singleton))
self.window = window
window.makeKeyAndVisible()
}
}
}
To enable your SwiftUI views to update when changing isLoading, declare a variable in the view's struct, like this:
struct MyView: View {
#EnvironmentObject var singleton: LoadingSingleton
var body: some View {
//Do something with singleton.isLoading
}
}
When you want to change the value of isLoading, just access it via SceneDelegate.singleton.isLoading, or, inside a SwiftUI view, via singleton.isLoading.

SwiftUI re-initialize EnvironmentObject?

How can I refresh an environment var in SwiftUI? It is easy to update any object that's a part of an environment object, but it seems like there should be a way to re-initialize.
struct reinitenviron: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
Text("refresh").onTapGesture {
globalObj = GlobalClass() //error here
}
}
}
The following gives an error that globalObj is get only. Is it possible to re-initialize?
A possible solution is to introduce explicit method in GlobalClass to reset it to initial state and use that method and in init and externally, like
class GlobalClass: ObservableObject {
#Published var value: Int = 1
init() {
self.reset()
}
func reset() {
self.value = 1
// do other activity if needed
}
}
struct reinitenviron: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
Text("refresh").onTapGesture {
globalObj.reset() // << here
}
}
}

How can SwiftUI Views change if the updated variable isn’t marked with a $?

In many cases in SwiftUI, values are marked with a $ to indicate that they’re a Binding variable, and allow them to update dynamically. Here’s an example of this behavior:
class Car: ObservableObject {
#Published var isReadyForSale = true
}
struct SaleView: View {
#Binding var isOn: Bool
var body: some View {
Toggle("Ready for Sale", isOn: $isOn)
}
}
struct ContentView: View {
#ObservedObject var car: Car
var body: some View {
Text("Details")
.font(.headline)
SaleView(isOn: $car.isReadyForSale) // generates a Binding to 'isReadyForSale' property
}
}
The $ is used twice to allow the Toggle to change whether the car is ready for sale, and for the car’s status to update the Toggle.
However, some values seem to update without the $. For instance, in this tutorial about different property wrappers, they show the following example:
class TestObject: ObservableObject {
#Published var num: Int = 0
}
struct ContentView: View {
#StateObject var stateObject = TestObject()
var body: some View {
VStack {
Text("State object: \(stateObject.num)")
Button("Increase state object", action: {
stateObject.num += 1
print("State object: \(stateObject.num)")
})
}
.onChange(of: stateObject.num) { newStateObject in
print("State: \(newStateObject)")
}
}
}
Why does it use Text("State object: \(stateObject.num)") and not Text("State object: \($stateObject.num)") with a dollar sign prefix? It was my understanding when you wanted a view to automatically update when a variable it uses changes, you prefix it with a dollar sign. Is that wrong?

Strange behaviour of ObservedObject in SwiftUI when using Timer

In the following program, Bar's initializer is called for each timer event. Does anyone know the reason of this problem?
This problem happens in both simulators and real devices iOS 13.5. I tested this on Xcode 11.5.
import SwiftUI
import Combine
class Foo: ObservableObject {
#Published var value: Int
init() {
print("init")
self.value = 10
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
self.value += 1
}
}
}
class Bar: ObservableObject {
#Published var value: Int
init() {
print("Bar")
self.value = 100
}
}
struct FirstView: View {
#EnvironmentObject var foo: Foo
#ObservedObject var bar = Bar()
var body: some View {
VStack {
Text("\(foo.value)")
Text("\(bar.value)")
}
}
}
struct ContentView: View {
#EnvironmentObject var foo: Foo
var body: some View {
VStack {
Text("\(foo.value)")
FirstView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any time Foo, being an ObservedObject, publishes a change, it causes the views that "observe" it to re-render their body
Foo is observed in ContentView, so a change in Foo makes it re-compute its body:
VStack {
Text("\(foo.value)")
FirstView()
}
This calls FirstView(), which in turn calls Bar().

What's the purpose of .environmentObject() view operator vs #EnvironmentObject?

I'm attempting to crawl out of the proverbial Neophyte abyss here.
I'm beginning to grasp the use of #EnvironmentObject till I notice the .environmentObject() view operator in the docs.
Here's my code:
import SwiftUI
struct SecondarySwiftUI: View {
#EnvironmentObject var settings: Settings
var body: some View {
ZStack {
Color.red
Text("Chosen One: \(settings.pickerSelection.name)")
}.environmentObject(settings) //...doesn't appear to be of use.
}
func doSomething() {}
}
I tried to replace the use of the #EnvironmentObject with the .environmentObject() operator on the view.
I got a compile error for missing 'settings' def.
However, the code runs okay without the .environmentObject operator.
So my question, why have the .environmentObject operator?
Does the .environmentObject() instantiates an environmentObject versus the #environmentObject accesses the instantiated object?
Here is demo code to show variants of EnvironmentObject & .environmentObject usage (with comments inline):
struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView().environmentObject(Settings()) // environment object injection
}
}
class Settings: ObservableObject {
#Published var foo = "Foo"
}
struct RootView: View {
#EnvironmentObject var settings: Settings // declaration for request of environment object
#State private var showingSheet = false
var body: some View {
VStack {
View1() // environment object injected implicitly as it is subview
.sheet(isPresented: $showingSheet) {
View2() // sheet is different view hierarchy, so explicit injection below is a must
.environmentObject(self.settings) // !! comment this out and see run-time exception
}
Divider()
Button("Show View2") {
self.showingSheet.toggle()
}
}
}
}
struct View1: View {
#EnvironmentObject var settings: Settings // declaration for request of environment object
var body: some View {
Text("View1: \(settings.foo)")
}
}
struct View2: View {
#EnvironmentObject var settings: Settings // declaration for request of environment object
var body: some View {
Text("View2: \(settings.foo)")
}
}
So, in your code ZStack does not declare needs of environment object, so no use of .environmentObject modifier.