SwiftUI - Updating #State when Global changes - swiftui

I'd like to update an UI element on an overview view when data on another view is changed.
I looked into #EnvironmentalObject and #Binding. However, an update to either object does not appear to force a view reload. Only changes to #State force renders.
Also, in the case described below, the ChangeView is not a child of OverviewView. Therefore #Binding is not an option anyway.
Data.swift
struct ExampleData : Hashable {
var id: UUID
var name: String
}
var globalVar: ExampleData = ExampleData(id: UUID(), name:"")
OverviewView.swift
struct OverviewView: View {
#State private var data: ExampleData = globalVar
var body: some View {
Text(data.name)
}
}
ChangeView.swift
struct ChangeView: View {
#State private var data: ExampleData = globalVar
var body: some View {
TextField("Name", text: $data.name, onEditingChanged: { _ in
globalVar = data }, onCommit: { globalVar = data })
}
}
Changes within the ChangeView TextField will update the globalVar. However, this will not update the Text on the OverviewView when switching back to the view.
I am aware that using global variables is "ugly" coding. How do I handle data that will be used in a multitude of unrelated views?
Please advise on how to better handle such a situation.

OverviewView and ChangeView hold different copies of the ExampleData struct in their data variables (When assigning a struct to another variable, you're effectively copying it instead of referencing it like an object.) so changing one won't affect the other.
#EnvironmentObject suits your requirements.
Here's an example:
Since, we're using #EnvironmentObject, you need to either convert ExampleData to
a class, or use a class to store it. I'll use the latter.
class ExampleDataHolder: ObservableObject {
#Published var data: ExampleData = ExampleData(id: UUID(), name:"")
}
struct CommonAncestorOfTheViews: View {
var body: some View {
CommonAncestorView()
.environmentObject(ExampleDataHolder())
}
}
struct OverviewView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
Text(dataHolder.data.name)
}
}
struct ChangeView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
TextField("Name", text: $dataHolder.data.name)
}
}

Related

Is this the right way for using #ObservedObject and #EnvironmentObject?

Cow you give me some confirmation about my understanding about #ObservedObject and #EnvironmentObject?
In my mind, using an #ObservedObject is useful when we send data "in line" between views that are sequenced, just like in "prepare for" in UIKit while using #EnvironmentObject is more like "singleton" in UIKit. My question is, is my code making the right use of these two teniques? Is this the way are applied in real development?
my model used as brain for funcions (IE urls sessions, other data manipulations)
class ModelClass_ViaObservedObject: ObservableObject {
#Published var isOn: Bool = true
}
class ModelClass_ViaEnvironment: ObservableObject {
#Published var message: String = "default"
}
my main view
struct ContentView: View {
//way to send data in views step by step
#StateObject var modelClass_ViaObservedObject = ModelClass_ViaObservedObject()
//way to share data more or less like a singleton
#StateObject var modelClass_ViaEnvironment = ModelClass_ViaEnvironment()
var myBackgroundColorView: Color {
if modelClass_ViaObservedObject.isOn {
return Color.green
} else {
return Color.red
}
}
var body: some View {
NavigationView {
ZStack {
myBackgroundColorView
VStack {
NavigationLink(destination:
SecondView(modelClass_viaObservedObject: modelClass_ViaObservedObject)
) {
Text("Go to secondary view")
.padding()
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.black, lineWidth: 1)
)
}
Text("text received from second view: \(modelClass_ViaEnvironment.message)")
}
}
.navigationTitle("Titolo")
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(modelClass_ViaEnvironment)
}
}
my second view
struct SecondView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var modelClass_viaObservedObject: ModelClass_ViaObservedObject
//global data in environment, not sent step by step view by view
#EnvironmentObject var modelClass_ViaEnvironment: ModelClass_ViaEnvironment
var body: some View {
VStack(spacing: 5) {
Text("Second View")
Button("change bool for everyone") {
modelClass_viaObservedObject.isOn.toggle()
dismiss()
}
TextField("send back", text: $modelClass_ViaEnvironment.message)
Text(modelClass_ViaEnvironment.message)
}
}
}
No, we use #State for view data like if a toggle isOn, which can either be a single value itself or a custom struct containing multiple values and mutating funcs. We pass it down the View hierarchy by declaring a let in the child View or use #Binding var if we need write access. Regardless of if we declare it let or #Binding whenever a different value is passed in to the child View's init, SwiftUI will call body automatically (as long as it is actually accessed in body that is).
#StateObject is for when a single value or a custom struct won't do and we need a reference type instead for view data, i.e. if persisting or syncing data (not using the new async/await though because we use .task for that). The object is init before body is called (usually before it is about to appear) and deinit when the View is no longer needed (usually after it disappears).
#EnvironmentObject is usually for the store object that holds model structs in #Published properties and is responsible for saving or syncing,. The difference is the model data is not tied to any particular View, like #State and #StateObject are for view data. This object is usually a singleton, one for the app and one with sample data for when previewing, because it should never be deinit. The advantage of #EnvironmentObject over #ObservedObject is we don't need to pass it down through each View as a let that don't need the object when we only need it further down the hierarchy. Note the reason it has to be passed down as a let and not #ObservedObject is then body would be needlessly called in the intermediate Views because SwiftUI's dependency tracking doesn't work for objects only value types.
Here is some sample code:
struct MyConfig {
var isOn = false
var message = ""
mutating func reset() {
isOn = false
message = ""
}
}
struct MyView: View {
#State var config = MyConfig() // grouping vars into their struct makes use of value semantics to track changes (a change to any of its properties is detected as a change to the struct itself) and offers testability.
var body: some View {
HStack {
ViewThatOnlyReads(config: config)
ViewThatWrites(config: $config)
}
}
}
struct ViewThatOnlyReads: View {
let config: MyConfig
var body: some View {
Text(config.isOn ? "It's on" : "It's off")
}
}
struct ViewThatWrites: View {
#Binding var config: MyConfig
var body: some View {
Toggle("Is On", isOn: $config.isOn)
}
}

SwiftUI pass Binding by ref to a child ViewModel

In SwiftUI, I am trying to create some binding between a parent ViewModel and a child ViewModel, here is a simplified example of my scenario:
The parent component:
class ParentViewModel : ObservableObject {
#Published var name = "John Doe"
func updateName() {
self.name = "Jonnie Deer"
}
}
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
ChildView(name: $viewModel.name)
// tapping the button the text on parent view is updated but not in child view
Button("Update", action: viewModel.updateName)
}
}
}
The child component:
class ChildViewModel : ObservableObject {
var name: Binding<String>
var displayName: String {
get {
return "Hello " + name.wrappedValue
}
}
init(name: Binding<String>) {
self.name = name
}
}
struct ChildView: View {
#StateObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
init(name: Binding<String>) {
_viewModel = StateObject(wrappedValue: ChildViewModel(name: name))
}
}
So, as stated in the comments, when I tap the button on the parent component the name is not getting updated in ChildView, as if the binding is lost...
Is there any other way to update view model with the updated value? say something like getDerivedStateFromProps in React (becuase when tapping the button the ChildView::init method is called with the new name.
Thanks.
Apple is very big on the concept of a Single Source of Truth(SSoT), and keeping it in mind will keep you from getting into the weeds in code like this. The problem you are having is that while you are using a Binding to instantiate the child view, you are turning around and using it as a #StateObject. When you do that, you are breaking the connection as #StateObject is supposed to sit at the top of the SSoT hierarchy. It designates your SSoT. Otherwise, you have two SSoTs, so you can only update one. The view model in ChildView should be an #ObservedObject so that it connects back up the hierarchy. Also, you can directly instantiate the ChildViewModel when you call ChildView. The initializer just serves to decouple things. Your views would look like this:
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
// You can directly use the ChildViewModel to instantiate the ChildView
ChildView(viewModel: ChildViewModel(name: $viewModel.name))
Button("Update", action: viewModel.updateName)
}
}
}
struct ChildView: View {
// Make this an #ObservedObject not a #StateObject
#ObservedObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
}
Neither view model is changed.
Get rid of the view model objects and do #State var name = “John” in ParentView and #Binding var name: String in ChildView. And pass $name into ChildView’s init which gives you write access as if ParentView was a view model object.
By using #State and #Binding you get the reference type semantics you want inside a value type which is the power of SwiftUI. If you just use objects you lose that benefit and have more work to do.
We usually only use ObservableObject for model data but we can also use it for loaders/fetchers where we want to tie some controller behaviour to the view lifecycle but for data transient to a view we always use #State and #Binding. You can extract related vars into their own struct and use mutating funcs for other logic and thus have a single #State struct used by body instead of multiple. This way it can still be testable like a view model object in UIKit would be.

Passing a ColorPicker selection to a different view in SwiftUI

I think I'm close but also missing something fundamental here with SwiftUI and passing data.
I have a top-level Color var called "masterColor" which I house in my "DataModel" struct.
Then in my view "NightLight", I have a system "ColorPicker", where I use a local var "localColor" to reflects whatever value the ColorPicker has.
Finally I have a "BackgroundControllerView", which sets a background color (which I would like to read the dataModel.masterColor)
What I'm trying to do set the dataModel.masterColor (which my whole app can see) equal to my localColor, which only NightLight can see. I've simplified the structure here a bit but the thrust of the question is about how to take local data and set something global equal to it for the rest of the app to see.
struct DataModel {
#State var masterColor = Color.red
}
struct NightLight: View {
#Binding var dataModel: DataModel
#State var localColor = Color.blue
var body: some View {
ColorPicker("Pick Color", selection: $localColor)
// Question: somehow set dataModel.masterColor = localColor ??
}
}
struct BackgroundControllerView: View {
#Binding var dataModel: DataModel
var body: some View {
Rectangle()
.fill(dataModel.masterColor)
}
}
Very much appreciate any help!
There's no need to have a separate localColor. You can directly pass in $dataModel.masterColor to the picker.
struct NightLight: View {
#Binding var dataModel: DataModel /// no need for extra `localColor`
var body: some View {
ColorPicker("Pick Color", selection: $dataModel.masterColor)
}
}
Also, remove the #State from masterColor -- you want the #State to be applied to DataModel, not the properties inside.
struct DataModel {
var masterColor = Color.red /// no #State here
}
struct NightLight: View {
#Binding var dataModel: DataModel
var body: some View {
ColorPicker("Pick Color", selection: $dataModel.masterColor)
}
}
struct BackgroundControllerView: View {
#Binding var dataModel: DataModel
var body: some View {
Rectangle()
.fill(dataModel.masterColor)
}
}
struct ContentView: View {
#State var dataModel = DataModel() /// use `#State` here instead
var body: some View {
VStack {
NightLight(dataModel: $dataModel)
BackgroundControllerView(dataModel: $dataModel)
}
}
}
Result:

Classes and Observable Object

I'm trying to make a data model class that can be referenced by different views. The data model has a function that can modify one of its published variables. However, this function is called inside one view, the change it makes to the published variable is not reflected in other views which also reference the class. The most simple example I can come up with is this:
struct ContentView: View {
var body: some View {
VStack {
TextView()
ButtonView()
}
}
}
struct TextView: View {
#ObservedObject var data = Data()
var body: some View {
Text(data.currentWord)
}
}
struct ButtonView: View {
#ObservedObject var data = Data()
var body: some View {
Button(action: {self.data.randomWord()}) {
Text("Random word")
}
}
}
class Data: ObservableObject {
#Published var currentWord = "Cat"
func randomWord() {
let word = ["Cat", "Dog", "Mouse", "Horse"].randomElement()!
print(word)
currentWord = word
}
}
Both the ButtonView and TextView reference the same class, and the ButtonView calls the 'Data' class's method 'randomWord' which modifies its 'currentWord' published variable. However, the change to this variable is not reflected in the Text of the TextView which also references the 'Data' class.
I think I'm not understanding something about classes and observableObject correctly. Would anyone be kind enough to point out my mistake here?
You create two different instance of Data in your subviews, instead you need to share one, so create it in ContentView and pass to subviews as below
struct ContentView: View {
#ObservedObject var data = Data()
var body: some View {
VStack {
TextView(data: data)
ButtonView(data: data)
}
}
}
struct TextView: View {
#ObservedObject var data: Data
var body: some View {
Text(data.currentWord)
}
}
struct ButtonView: View {
#ObservedObject var data: Data
var body: some View {
Button(action: {self.data.randomWord()}) {
Text("Random word")
}
}
}
Also, as variant, for such scenario can be used EnvironmentObject pattern. There are a lot of examples here on SO you can find about environment objects usage - just search by keywords.

SwiftUI dynamically pop NavigationLink with a list of links

I want to pop a NavigationLink from within code. I followed this article and it works for one link (https://swiftui-lab.com/bug-navigationlink-isactive/). However, when using a list of links, one has to use a separate boolean for each NavigationLink.
So I thought the smart way to do this is with an EnvironmentObject that holds a dictionary with a boolean for each ChildView:
class Navigation: ObservableObject{
#Published var show:[UUID:Bool] = [:]
}
Let's say we want to have a variable number child views (of type MyObject).
What needs to be changed in order to make it work?
struct MyObject:Identifiable{
var id = UUID()
var name:String
}
struct ContentView: View {
#EnvironmentObject var navigation:Navigation
var objects = [MyObject(name: "view1"), MyObject(name: "view2"), MyObject(name: "view3")]
init() {
for object in objects{
self.navigation.show[object.id] = false
}
}
var body: some View {
NavigationView{
VStack{
ForEach(objects, id:\.self.id){ object in
NavigationLink(destination: Child(object:object), isActive: self.$navigation.show[object.id], label: {
Text(object.name)
})
}
}
}
}
}
struct Child: View {
#EnvironmentObject var navi:Navigation
var object:MyObject
var body: some View {
Button(action:{self.navi.show[self.object.id] = false}, label: {
Text("back")
})
}
}
The view that the NavigationLink navigates to has an environment variable set called presentationMode. This variable lets you programatically pop your child view back to the parent view.
So instead of having to keep track of all the display states, we can simplify your Child struct to something like this:
struct Child: View {
#Environment(\.presentationMode) private var presentation
var object:MyObject
var body: some View {
Button(action:{ self.presentation.wrappedValue.dismiss() }, label: {
Text("back")
})
}
}