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
}
}
}
Related
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)
}
}
}
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.
in iOS13 I do the following to bind my View to my model:
class MyModel: ObservableObject {
#Published var someVar: String = "initial value"
}
struct MyView: View {
#ObservedObject var model = MyModel()
var body: some View {
Text("the value is \(model.someVar)")
}
}
in iOS14 there is a new property wrapper called #StateObject that I can use in the place of #ObservedObject, I need this snippet of code to be compatible with iOS13 and iOS14 while leveraging iOS14's new feature, how can I do that with #StateObject for the same variable ?
Different property wrappers generate different types of hidden properties, so you cannot just conditionally replace them. Here is a demo of possible approach.
Tested with Xcode 12 / iOS 14 (deployment target 13.6)
struct ContentView: View {
var body: some View {
if #available(iOS 14, *) {
MyNewView()
} else {
MyView()
}
}
}
class MyModel: ObservableObject {
#Published var someVar: String = "initial value"
}
#available(iOS, introduced: 13, obsoleted: 14, renamed: "MyNewView")
struct MyView: View {
#ObservedObject var model = MyModel()
var body: some View {
CommonView().environmentObject(model)
}
}
#available(iOS 14, *)
struct MyNewView: View {
#StateObject var model = MyModel()
var body: some View {
CommonView().environmentObject(model)
}
}
struct CommonView: View {
#EnvironmentObject var model: MyModel
var body: some View {
Text("the value is \(model.someVar)")
}
}
ObservableObject and #Published are part of the Combine framework and you should only use those when you require a Combine pipeline to assign the output to the #Published var. What you should be using for your data is #State use it as follows:
struct MyView: View {
#State var text = "initial value"
var body: some View {
VStack{
Text("the value is \(text)")
TextField("", text: $text)
}
}
}
If you have multiple vars or need functions then you should refactor these into their own struct. Multiple related properties in their own struct makes the View more readable, can maintain invariance on its properties and be tested independently. And because the struct is a value type, any change to a property, is visible as a change to the struct (Learn this in WWDC 2020 Data Essentials in SwiftUI ). Implement as follows:
struct MyViewConfig {
var text1 = "initial value"
var text2 = "initial value"
mutating func reset(){
text1 = "initial value"
text2 = "initial value"
}
}
struct MyView: View {
#Binding var config: MyViewConfig
var body: some View {
VStack{
Text("the value is \(config.text1)")
TextField("", text: $config.text1)
Button("Reset", action: reset)
}
}
func reset() {
config.reset()
}
}
struct ContentView {
#State var config = MyViewConfig()
var body: some View {
MyView(config:$config)
}
}
SwiftUI is designed to take advantage of value semantics where all the data is in structs which makes it run super fast. If you unnecessarily create objects then you are slowing it all down.
To answer the question, that use of ObservableObject to init is in correct. Every time the View struct is init a new object will also be init which will slow down SwiftUI. You’ll need to either use a global var or singleton to store the objects and use onAppear to init and onDissapear to destroy. In the WWDC video where StateObject is introduced you’ll hear him say “you don’t need to mess with onDissapear anymore” so that is your clue on how to simulate StateObject. By the way, it is totally fine to update your app with new features only available in the new OS, users that have not yet updated their OS will just stay on the old version of the app and is a much simpler way to work.
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
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.