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