SwiftUI Environment Object Not Getting Updated - ios

I thought it was the case that #Published properties inside a class designated as an environment object would automatically update to all the subviews I pass the environment object to, but the below code isn't updating, what am I doing wrong?
class trackerDataStore: ObservableObject {
let healthStore = HKHealthStore()
#Published var isLoadingWorkouts = false
#Published var subscriptionIsActive = false
#Published var trackerWorkoutObject: trackerWorkoutObject?
}
struct detailHeaderCard: View {
#EnvironmentObject var trackerDataStore: TrackerDataStore
var body: some View {
//omitted
}
.sheet(isPresented: $isShowingPlayerAddStatsFormAsModal) {
PlayerAddStatsForm(isShowingPlayerAddStatsFormAsModal: $isShowingPlayerAddStatsFormAsModal)
.environmentObject(trackerDataStore)
}
}
struct PlayerAddStatsForm: View {
#EnvironmentObject var trackerDataStore: TrackerDataStore
//not getting reactively updated here
}

I found out that the way I wired up #EnvironmentObject actually works well, I had a bug in my code in another area that was preventing the view from receiving the data I was looking for that made me think it was related to #EnvironmentObject not working properly.

Related

SwiftUI Dependency Injection

I have a SwiftUI app, its a tab based app
struct Tab_View: View {
var body: some View {
TabView{
Main1_View().tabItem {
Text("Blah 1")
Image("TabBar1")
}
Main2_View().tabItem {
Text("Blah 2")
Image("TabBar2")
}
}
}
}
Each view has its own view controller
struct Main1_View: View {
#ObservedObject var viewModel: Main1_ViewModel = Main1_ViewModel()
var body: some View {
VStack(spacing:0){
<<< VIEW CODE >>>
}
}
}
ViewModel Example
class Main1_ViewModel: ObservableObject {
#ObservableObject var settings: GameSettings
func Randomise(){
dataSource = settings.selectedFramework;
}
}
The class GameSettings is used by multiple viewmodels, is an ObservableObject, same instance of the class everywhere.
My background is C# and using CastleWindsor for dependency injection.
My Question: Is there a SwiftUI equivalent to pass around an instance of GameSettings?
Because of your requirement to use an ObservableObject (GameSettings) inside another ObservableObject (the view model) and use dependency injection, things are going to be a little bit convoluted.
In order to get a dependency-injected ObservableObject to a View, the normal solution is to use #EnvironmentObject. But, then you'll have to pass the object from the view to its view model. In my example, I've done that in onAppear. The side effect is that the object is an optional property on the view model (you could potentially solve this by setting a dummy initial value).
Because nested ObservableObjects don't work out-of-the box with #Published types (which work with value types, not reference types), you'll want to make sure that you use objectWillChange to pass along any changes from GameSettings to the parent view model, which I've done using Combine.
The DispatchQueue.main.asyncAfter part is there just to show that the view does in fact update when the value inside GameSettings is changed.
(Note, I've also changed your type names to use the Swift conventions of camel case)
import SwiftUI
import Combine
struct ContentView: View {
#StateObject private var settings = GameSettings()
var body: some View {
TabView {
Main1View().tabItem {
Text("Blah 1")
Image("TabBar1")
}
Main2View().tabItem {
Text("Blah 2")
Image("TabBar2")
}
}.environmentObject(settings)
}
}
struct Main1View: View {
#EnvironmentObject var settings: GameSettings
#StateObject var viewModel: Main1ViewModel = Main1ViewModel()
var body: some View {
VStack(spacing:0){
Text("Game settings: \(viewModel.settings?.myValue ?? "no value")")
}.onAppear {
viewModel.settings = settings
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
settings.myValue = "changed"
}
}
}
}
class GameSettings : ObservableObject {
#Published var myValue : String = "Test"
}
class Main1ViewModel: ObservableObject {
private var cancellable : AnyCancellable?
var settings: GameSettings? {
didSet {
print("Running init on Main1ViewModel")
self.objectWillChange.send()
cancellable = settings?.objectWillChange.sink(receiveValue: { _ in
print("Sending...")
self.objectWillChange.send()
})
}
}
}
struct Main2View : View {
var body: some View {
Text("Hello, world!")
}
}
Dependency injection, DI is the practice of providing an object with the other objects it depends on rather than creating them internally.
My Opinion
For SwiftUI you can use #EnvironmentObject. #EnvironmentObject and the View Model Factory both provide a clean solution to this.
Check this tutorial
https://mokacoding.com/blog/swiftui-dependency-injection/

How to trigger #State object with respect to ObservableObject Object?

This are two properties which I declared
struct DashBoardView: View {
#State var isToPush: Bool = false
#ObservedObject var sideBarHandler = SideBarHandler()
Where SideBarHandler is -
class SideBarHandler: ObservableObject {
#Published var isListItemClicked:Bool = false
}
Now I am looking to activate $isToPush based on sideBarHandler.isListItemClicked
Because I want to bind it here
NavigationLink(destination: FavouriteView(), isActive: $isToPush) {// NavLink
You have to consider where the source of truth is for the data.
Based on your description, it seems that the isListItemClicked is the source of truth, so you shouldn't even need a #State variable - use $sideBarHandler.isListItemClicked directly (prefix $ of an #ObservedObject gives you a Binding):
NavigationLink(destination: FavouriteView(), isActive: $sideBarHandler.isListItemClicked)
Of course, if #State var isToPush: Bool is only affected by sideBarHandler.isListItemClicked but otherwise exists independently - i.e. it is a source of truth for this data - then you can use onReceive as suggested by #Asperi to change the isToPush property:
.onReceive(sideBarHandler.$isListItemClicked) {
isToPush = $0
}
(note that $ prefix here accesses a #Published Combine publisher)
Also, unrelated to your question, but if you're instantiating an ObservableObject inside the view, then you should use #StateObject instead of #ObservedObject. The latter is meant for a case when the observable object is created outside of the view.
You can use onReceive somewhere in body, like
struct DashBoardView: View {
#State var isToPush: Bool = false
#ObservedObject var sideBarHandler = SideBarHandler()
var body: some View {
VStack {
// some content here
}
.onReceive(sideBarHandler.$isListItemClicked) {
isToPush = $0
}
}
}

SwiftUI / Combine Pass data between two models

I have question regarding how to pass data between two models.
struct SettingsCell: View {
#State var isOn: Bool
var body: some View {
Toggle(name, isOn: $isOn)
}
}
class SettingsModel: ObservableObject {
#Published var someValue: Bool = false
}
struct SettingsView: View {
#ObservedObject var model = SettingsModel()
var body: some View {
List {
SettingsCell(isOn: model.someValue)
}
}
}
So i want to pass isOn state from cell, to main model, and react there. Send requests for example.
You need to declare isOn as #Binding in SettingsCell.
#State should only be used for properties initialised inside the View itself and must always be private. If you want to pass in a value that should update the View whenever it changes, but the value is created outside the View, you need to use Binding.
Another really important thing to note is that #ObservedObjects must always be injected into Views, you must not initialise them inside the view itself. This is because whenever an #ObservedObject is updated, it updates the view itself, so if you initialised the object inside the view, whenever the object updates the view, the view would create a new #ObservedObject and hence your changes wouldn't be persisted from the view to the model.
If you are targeting iOS 14 and want to create the model inside the view, you can use #StateObject instead.
struct SettingsCell: View {
#Binding private var isOn: Bool
init(isOn: Binding<Bool>) {
self._isOn = isOn
}
var body: some View {
Toggle(name, isOn: $isOn)
}
}
class SettingsModel: ObservableObject {
#Published var someValue: Bool = false
}
struct SettingsView: View {
#ObservedObject private var model: SettingsModel
init(model: SettingsModel) {
self.model = model
}
var body: some View {
List {
SettingsCell(isOn: $model.someValue)
}
}
}
Binding is used in cases where the data is "owned" by a parent view - i.e. the parent holds the source of truth - and needs the child view to update it:
struct SettingsCell: View {
#Binding var isOn: Bool // change to Binding
var body: some View {
Toggle(name, isOn: $isOn)
}
}
struct SettingsView: View {
// unrelated, but better to use StateObject
#StateObject var model = SettingsModel()
var body: some View {
List {
// pass the binding by prefixing with $
SettingsCell(isOn: $model.someValue)
}
}
}

Embedded #Binding does not changing a value

Following this cheat sheet I'm trying to figure out data flow in SwiftUI. So:
Use #Binding when your view needs to mutate a property owned by an ancestor view, or owned by an observable object that an ancestor has a reference to.
And that is exactly what I need so my embedded model is:
class SimpleModel: Identifiable, ObservableObject {
#Published var values: [String] = []
init(values: [String] = []) {
self.values = values
}
}
and my View has two fields:
struct SimpleModelView: View {
#Binding var model: SimpleModel
#Binding var strings: [String]
var body: some View {
VStack {
HStack {
Text(self.strings[0])
TextField("name", text: self.$strings[0])
}
HStack {
Text(self.model.values[0])
EmbeddedView(strings: self.$model.values)
}
}
}
}
struct EmbeddedView: View {
#Binding var strings: [String]
var body: some View {
VStack {
TextField("name", text: self.$strings[0])
}
}
}
So I expect the view to change Text when change in input field will occur. And it's working for [String] but does not work for embedded #Binding object:
Why it's behaving differently?
Make property published
class SimpleModel: Identifiable, ObservableObject {
#Published var values: [String] = []
and model observed
struct SimpleModelView: View {
#ObservedObject var model: SimpleModel
Note: this in that direction - if you introduced ObservableObject then corresponding view should have ObservedObject wrapper to observe changes of that observable object's published properties.
In SimpleModelView, try changing:
#Binding var model: SimpleModel
to:
#ObservedObject var model: SimpleModel
#ObservedObjects provide binding values as well, and are required if you want state changes from classes conforming to ObservableObject

SwiftUI and Combine what is the difference between Published and Binding

I know in which situations to use #Binding and #Published
like in ObservableObject I generally use #Published, or objectWillChange.send()
And #Binding in subviews to propagate changes to parent
But here I have snippet which seems to be working that uses both #Binding and #Published
in ObservableObject
I consider what is the difference.
#Binding var input: T
#Binding var validation: Validation
#Published var value: T {
didSet {
self.input = self.value
self.validateField()
}
}
init(input: Binding<T>, rules: [Rule<T>], validation: Binding<Validation>) {
self._input = input
self.value = input.wrappedValue
self.rules = rules
self._validation = validation
}
As I tested it seems that if I bind TextField to #Published then didSet is called but if I bind it to #Binding then didSet won't be called.
For a #Binding, I found that didSet was called if you assign to the property directly. Try running this example in an xcode playground:
import SwiftUI
import Combine
struct MyView: View {
#Binding var value: Int {
didSet {
print("didSet", value)
}
}
var body: some View { Text("Hello") }
}
var myBinding = Binding<Int>(
get: { 4 },
set: { v in print("set", v)})
let view = MyView(value: myBinding)
view.value = 99
Output:
set 99
didSet 4
Regarding the difference between #Published and #Binding:
You'll generally use #Binding to pass a binding that originated from some source of truth (like #State) down the view hierarchy.
Use #Published in an ObservableObject to allow a view to react to changes to a property.

Resources