'Passing An ObservedObject To Nested Child Views SwiftUI (SwiftUI Data Flow)
I'm trying to understand why passing an @ObservedObject variable does not work for nested child views. The data is able to be passed but the changes only reflects at the root view where the @ObservedObject variable was created. The changes don't show in the subviews. Looking at Apple documentation (which has been updated for Xcode beta 5), the answer seems to be to create both an environment object and a regular variable in order to get the correct index from the environment object. Here is Apples example.
My understanding is that an @ObservedObject can be used to reference a variable from multiple views, but if you want the data to be accessible from any view then you should use an environment object. Therefore I believe that passing the @ObservedObject should be possible. The issue I believe is happening is that since ScreenTwo is passing the @Binding variable to DetailsView and that is what's causing a problem. To solve this I would think you need to keep passing the full @ObservedObject, but then again you would need some type of regular variable to get the proper index.
I feel like all of this should be much more straightforward.
import SwiftUI
import Combine
struct Sport: Identifiable{
var id = UUID()
var name : String
var isFavorite = false
var school : String
}
final class SportData: ObservableObject {
@Published var store =
[
Sport(name: "soccer", isFavorite: false, school: "WPI"),
Sport(name: "tennis", isFavorite: false, school: "WPI"),
Sport(name: "swimming", isFavorite: true, school: "WPI"),
Sport(name: "running", isFavorite: true, school: "RIT"),
]
}
struct Testing: View {
@ObservedObject var sports = SportData()
var body: some View {
NavigationView{
List{
ForEach($sports.store){ sport in
NavigationLink(destination: ScreenTwo(sport: sport)){
HStack {
Text(sport.value.name)
Spacer()
Text(sport.value.isFavorite.description)
}
}
}
}
}.navigationBarTitle("Settings")
}
}
struct ScreenTwo : View{
@Binding var sport : Sport
var body: some View{
NavigationLink(destination: DetailsView(sport: $sport)){
Text(sport.isFavorite.description)
}
}
}
struct DetailsView: View {
@Binding var sport : Sport
var body: some View {
Button(action: {
self.sport.isFavorite.toggle()
self.sport.name = "Ricky"
}) {
Text(sport.isFavorite.description)
Text(sport.name)
}
}
}
#if DEBUG
struct Testing_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}
#endif
Solution 1:[1]
For ObservableObject
the pairing ObservedObject
makes view refresh, so to solve the task in question I would recommend the following approach:
Demo
Code
import SwiftUI
import Combine
class Sport: ObservableObject, Hashable, Identifiable {
static func == (lhs: Sport, rhs: Sport) -> Bool {
lhs.name == rhs.name && lhs.isFavorite == rhs.isFavorite && lhs.school == rhs.school
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(isFavorite)
hasher.combine(school)
}
@Published var name : String
@Published var isFavorite = false
@Published var school : String
init(name: String, isFavorite: Bool, school: String) {
self.name = name
self.isFavorite = isFavorite
self.school = school
}
}
final class SportData: ObservableObject {
@Published var store =
[
Sport(name: "soccer", isFavorite: false, school: "WPI"),
Sport(name: "tennis", isFavorite: false, school: "WPI"),
Sport(name: "swimming", isFavorite: true, school: "WPI"),
Sport(name: "running", isFavorite: true, school: "RIT"),
]
}
struct TestingObservedObject: View {
@ObservedObject var sports = SportData()
var body: some View {
NavigationView{
List{
ForEach(sports.store){ sport in
NavigationLink(destination: ScreenTwo(sport: sport)) {
HStack {
Text("\(sport.name)")
Spacer()
Text(sport.isFavorite.description)
}
}
.onReceive(sport.$isFavorite) { _ in self.sports.objectWillChange.send() }
}
}
}.navigationBarTitle("Settings")
}
}
struct ScreenTwo : View{
@ObservedObject var sport : Sport
var body: some View{
NavigationLink(destination: DetailsView(sport: sport)){
Text(sport.isFavorite.description)
}
}
}
struct DetailsView: View {
@ObservedObject var sport : Sport
var body: some View {
Button(action: {
self.sport.isFavorite.toggle()
self.sport.name = "Ricky"
}) {
Text(sport.isFavorite.description)
Text(sport.name)
}
}
}
#if DEBUG
struct Testing_Previews: PreviewProvider {
static var previews: some View {
TestingObservedObject()
}
}
#endif
Solution 2:[2]
You should define sport field as @EnviromentObject
instead of @ObservedObject
on ScreenTwo
and DetailsView
. Also set enviroment object with NavigationLink(destination: ScreenTwo()).environmentObject(sport)
on TestingObservedObject
view.
Solution 3:[3]
@EnvironmentObject - A property wrapper type for an observable object supplied by a parent or ancestor view.
environmentObject(_:) - Supplies an ObservableObject to a view subhierachy.(this is a viewModifire)
So that we can share @ObservableObject through environmentObject(_:)
. To accept it, sub view should have @EnvironmentObject
(ex :- @EnvironmentObject var viewModel: MyViewModel)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | np2314 |
Solution 3 | Yodagama |