Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I access data from a child view as the parent view at any time in SwiftUI?

I'm new to SwiftUI and understand that I may need to implement EnvironmentObject in some way, but I'm not sure how in this case.

This is the Trade class

class Trade {
    var teamsSelected: [Team]

    init(teamsSelected: [Team]) {
        self.teamsSelected = teamsSelected
    }
}

This is the child view. It has an instance trade from the Trade class. There is a button that appends 1 to array teamsSelected.

struct TeamRow: View {
    var trade: Trade

    var body: some View {
        Button(action: {
            self.trade.teamsSelected.append(1)
        }) {
            Text("Button")
        }
    }
}

This is the parent view. As you can see, I pass trade into the child view TeamRow. I want trade to be in sync with trade in TeamRow so that I can then pass trade.teamsSelected to TradeView.

struct TeamSelectView: View {
    var trade = Trade(teamsSelected: [])

    var body: some View {
        NavigationView{
            VStack{
                NavigationLink(destination: TradeView(teamsSelected: trade.teamsSelected)) {
                   Text("Trade")
                }

                List {
                    ForEach(teams) { team in
                        TeamRow(trade: self.trade)
                    }
                }
            }
        }
    }
}
like image 206
Jnguyen22 Avatar asked Aug 22 '19 21:08

Jnguyen22


2 Answers

I've taken your code and changed some things to illustrate how SwiftUI works in order to give you a better understanding of how to use ObservableObject, @ObservedObject, @State, and @Binding.

One thing to mention up front - @ObservedObject is currently broken when trying to run SwiftUI code on a physical device running iOS 13 Beta 6, 7, or 8. I answered a question here that goes into that in more detail and explains how to use @EnvironmentObject as a workaround.


Let's first take a look at Trade. Since you're looking to pass a Trade object between views, change properties on that Trade object, and then have those changes reflected in every view that uses that Trade object, you'll want to make Trade an ObservableObject. I've added an extra property to your Trade class purely for illustrative purposes that I'll explain later. I'm going to show you two ways to write an ObservableObject - the verbose way first to see how it works, and then the concise way.

import SwiftUI
import Combine

class Trade: ObservableObject {
    let objectWillChange = PassthroughSubject<Void, Never>()

    var name: String {
        willSet {
            self.objectWillChange.send()
        }
    }

    var teamsSelected: [Int] {
        willSet {
            self.objectWillChange.send()
        }
    }

    init(name: String, teamsSelected: [Int]) {
        self.name = name
        self.teamsSelected = teamsSelected
    }
}

When we conform to ObservableObject, we have the option to write our own ObservableObjectPublisher, which I've done by importing Combine and creating a PassthroughSubject. Then, when I want to publish that my object is about to change, I can call self.objectWillChange.send() as I have on willSet for name and teamsSelected.

This code can be shortened significantly, however. ObservableObject automatically synthesizes an object publisher, so we don't actually have to declare it ourselves. We can also use @Published to declare our properties that should send a publisher event instead of using self.objectWillChange.send() in willSet.

import SwiftUI

class Trade: ObservableObject {
    @Published var name: String
    @Published var teamsSelected: [Int]

    init(name: String, teamsSelected: [Int]) {
        self.name = name
        self.teamsSelected = teamsSelected
    }
}

Now let's take a look at your TeamSelectView, TeamRow, and TradeView. Keep in mind once again that I've made some changes (and added an example TradeView) just to illustrate a couple of things.

struct TeamSelectView: View {
    @ObservedObject var trade = Trade(name: "Name", teamsSelected: [])
    @State var teams = [1, 1, 1, 1, 1]

    var body: some View {
        NavigationView{
            VStack{
                NavigationLink(destination: TradeView(trade: self.trade)) {
                    Text(self.trade.name)
                }

                List {
                    ForEach(self.teams, id: \.self) { team in
                        TeamRow(trade: self.trade)
                    }
                }
                Text("\(self.trade.teamsSelected.count)")
            }
            .navigationBarItems(trailing: Button("+", action: {
                self.teams.append(1)
            }))
        }
    }
}
struct TeamRow: View {
    @ObservedObject var trade: Trade

    var body: some View {
        Button(action: {
            self.trade.teamsSelected.append(1)
        }) {
            Text("Button")
        }
    }
}
struct TradeView: View {
    @ObservedObject var trade: Trade

    var body: some View {
        VStack {
            Text("\(self.trade.teamsSelected.count)")
            TextField("Trade Name", text: self.$trade.name)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
        }
    }
}

Let's first look at @State var teams. We use @State for simple value types - Int, String, basic structs - or collections of simple value types. @ObservedObject is used for objects that conform to ObservableObject, which we use for data structures that are more complex than just Int or String.

What you'll notice with @State var teams is that I've added a navigation bar item that will append a new item to the teams array when pressed, and since our List is generated by iterating through that teams array, our view re-renders and adds a new item to our List whenever the button is pressed. That's a very basic example of how you would use @State.

Next, we have our @ObservedObject var trade. You'll notice that I'm not really doing anything different than you were originally. I'm still creating an instance of my Trade class and passing that instance between my views. But since it's now an ObservableObject, and we're using @ObservedObject, our views will now all receive publisher events whenever the Trade object changes and will automatically re-render their views to reflect those changes.

The last thing I want to point out is the TextField I created in TradeView to update the Trade object's name property.

TextField("Trade Name", text: self.$trade.name)

The $ character indicates that I'm passing a binding to the text field. This means that any changes TextField makes to name will be reflected in my Trade object. You can do the same thing yourself by declaring @Binding properties that allow you to pass bindings between views when you are trying to sync state between your views without passing entire objects.

While I changed your TradeView to take @ObservedObject var trade, you could simply pass teamsSelected to your trade view as a binding like this - TradeView(teamsSelected: self.$trade.teamsSelected) - as long as your TradeView accepts a binding. To configure your TradeView to accept a binding, all you would have to do is declare your teamsSelected property in TradeView like this:

@Binding var teamsSelected: [Team]

And lastly, if you run into issues with using @ObservedObject on a physical device, you can refer to my answer here for an explanation of how to use @EnvironmentObject as a workaround.

like image 158
graycampbell Avatar answered Nov 19 '22 11:11

graycampbell


You can use @Binding and @State / @Published in Combine. In other words, use a @Binding property in Child View and bind it with a @State or a @Published property in Parent View as following.

struct ChildView: View {
    @Binding var property1: String
    var body: some View {
        VStack(alignment: .leading) {
            TextField(placeholderTitle, text: $property1)
        }        
    }
}
struct PrimaryTextField_Previews: PreviewProvider {
    static var previews: some View {
        PrimaryTextField(value: .constant(""))
    }
}
struct ParentView: View{
    @State linkedProperty: String = ""
    //...
        ChildView(property1: $linkedProperty)
    //...
}

or if you have a @Publilshed property in your viewModel(@ObservedObject), then use it to bind the state like ChildView(property1: $viewModel.publishedProperty).

like image 43
Kevin Avatar answered Nov 19 '22 10:11

Kevin