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

前端 未结 3 1358
暗喜
暗喜 2021-01-13 13:00

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 cla

相关标签:
3条回答
  • 2021-01-13 13:26

    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.

    0 讨论(0)
  • 2021-01-13 13:45

    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).

    0 讨论(0)
  • 2021-01-13 13:48

    firstly thanks a lot to graycampbell for giving me a better understanding! However, my understanding does not seem to work out completely. I have a slightly different case which I'm not fully able to solve.

    I've already asked my question in a separate thread, but I want to add it here as well, because it somehow fits the topic: Reading values from list of toggles in SwiftUI

    Maybe somebody of you guys can help me with this. The main difference to the initial post if this topic is, that I have to collect Data from each GameGenerationRow in the GameGenerationView and then hand it over to another View.

    0 讨论(0)
提交回复
热议问题