In SwiftUI, I am trying to create some binding between a parent ViewModel and a child ViewModel, here is a simplified example of my scenario:
The parent component:
class ParentViewModel : ObservableObject {
#Published var name = "John Doe"
func updateName() {
self.name = "Jonnie Deer"
}
}
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
ChildView(name: $viewModel.name)
// tapping the button the text on parent view is updated but not in child view
Button("Update", action: viewModel.updateName)
}
}
}
The child component:
class ChildViewModel : ObservableObject {
var name: Binding<String>
var displayName: String {
get {
return "Hello " + name.wrappedValue
}
}
init(name: Binding<String>) {
self.name = name
}
}
struct ChildView: View {
#StateObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
init(name: Binding<String>) {
_viewModel = StateObject(wrappedValue: ChildViewModel(name: name))
}
}
So, as stated in the comments, when I tap the button on the parent component the name is not getting updated in ChildView, as if the binding is lost...
Is there any other way to update view model with the updated value? say something like getDerivedStateFromProps in React (becuase when tapping the button the ChildView::init method is called with the new name.
Thanks.
Apple is very big on the concept of a Single Source of Truth(SSoT), and keeping it in mind will keep you from getting into the weeds in code like this. The problem you are having is that while you are using a Binding to instantiate the child view, you are turning around and using it as a #StateObject. When you do that, you are breaking the connection as #StateObject is supposed to sit at the top of the SSoT hierarchy. It designates your SSoT. Otherwise, you have two SSoTs, so you can only update one. The view model in ChildView should be an #ObservedObject so that it connects back up the hierarchy. Also, you can directly instantiate the ChildViewModel when you call ChildView. The initializer just serves to decouple things. Your views would look like this:
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
// You can directly use the ChildViewModel to instantiate the ChildView
ChildView(viewModel: ChildViewModel(name: $viewModel.name))
Button("Update", action: viewModel.updateName)
}
}
}
struct ChildView: View {
// Make this an #ObservedObject not a #StateObject
#ObservedObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
}
Neither view model is changed.
Get rid of the view model objects and do #State var name = “John” in ParentView and #Binding var name: String in ChildView. And pass $name into ChildView’s init which gives you write access as if ParentView was a view model object.
By using #State and #Binding you get the reference type semantics you want inside a value type which is the power of SwiftUI. If you just use objects you lose that benefit and have more work to do.
We usually only use ObservableObject for model data but we can also use it for loaders/fetchers where we want to tie some controller behaviour to the view lifecycle but for data transient to a view we always use #State and #Binding. You can extract related vars into their own struct and use mutating funcs for other logic and thus have a single #State struct used by body instead of multiple. This way it can still be testable like a view model object in UIKit would be.
Related
(You can skip this part and just look at the code.) I'm creating a complicated form. The form creates, say, a Post object, but I want to be able to create several Comment objects at the same time. So I have a Post form and a Comment form. In my Post form, I can fill out the title, description, etc., and I can add several Comment forms as I create more comments. Each form has an #ObservedObject viewModel of its own type. So I have one parent Post #ObservedObject viewModel, and another #ObservedObject viewModel for the array of the Comment objects which is also a #ObservedObject viewModel.
I hope that made some sense -- here is code to minimally reproduce the issue (unrelated to Posts/Comments). The objective is to make the count of the "Childish" viewModels at the parent level count up like how they count up for the "Child" view.
import Combine
import SwiftUI
final class ParentScreenViewModel: ObservableObject {
#Published var childScreenViewModel = ChildScreenViewModel()
}
struct ParentScreen: View {
#StateObject private var viewModel = ParentScreenViewModel()
var body: some View {
Form {
NavigationLink(destination: ChildScreen(viewModel: viewModel.childScreenViewModel)) {
Text("ChildishVMs")
Spacer()
Text("\(viewModel.childScreenViewModel.myViewModelArray.count)") // FIXME: this count is never updated
}
}
}
}
struct ParentScreen_Previews: PreviewProvider {
static var previews: some View {
ParentScreen()
}
}
// MARK: - ChildScreenViewModel
final class ChildScreenViewModel: ObservableObject {
#Published var myViewModelArray: [ChildishViewModel] = []
func appendAnObservedObject() {
objectWillChange.send() // FIXME: does not work
myViewModelArray.append(ChildishViewModel())
}
}
struct ChildScreen: View {
#ObservedObject private var viewModel: ChildScreenViewModel
init(viewModel: ChildScreenViewModel = ChildScreenViewModel()) {
self.viewModel = viewModel
}
var body: some View {
Button {
viewModel.appendAnObservedObject()
} label: {
Text("Append a ChildishVM (current num: \(viewModel.myViewModelArray.count))")
}
}
}
struct ChildScreen_Previews: PreviewProvider {
static var previews: some View {
ChildScreen()
}
}
final class ChildishViewModel: ObservableObject {
#Published var myProperty = "hey!"
}
ParentView:
ChildView:
I can't run this in previews either -- seems to need to be run in the simulator. There are lots of questions similar to this one but not quite like it (e.g. the common answer of manually subscribing to the child's changes using Combine does not work). Would using #EnvironmentObject help somehow? Thanks!
First get rid of the view model objects, we don't use those in SwiftUI. The View data struct is already the model for the actual views on screen e.g. UILabels, UITables etc. that SwiftUI updates for us. It takes advantage of value semantics to resolve consistency bugs you typically get with objects, see Choosing Between Structures and Classes. SwiftUI structs uses property wrappers like #State to make these super-fast structs have features like objects. If you use actual objects on top of the View structs then you are slowing down SwiftUI and re-introducing the consistency bugs that Swift and SwiftUI were designed to eliminate - which seems to me is exactly the problem you are facing. So it of course is not a good idea to use Combine to resolve consistency issues between objects it'll only make the problem worse.
So with that out of the way, you just need correct some mistakes in your design. Model types should be structs (these can be arrays or nested structs) and have a single model object to manage the life-cycle and side effects of the struct. You can have structs within structs and use bindings to pass them between your Views when you need write access, if you don't then its simply a let and SwiftUI will automatically call body whenever a View is init with a different let from last time.
Here is a basic example:
struct Post: Identifiable {
let id = UUID()
var text = ""
}
class Model: ObservableObject {
#Published var posts: [Post] = []
// func load
// func save
// func delete a post by ID
}
struct ModelController {
static let shared = ModelController()
let model = Model()
//static var preview: ModelController {
// ...
//}()
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ModelController.shared.model)
}
}
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
ForEach($model.posts) { $post in
ContentView2(post: post)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(ModelController.shared.preview)
}
}
struct ConventView2: View {
#Binding var post: Post
var body: some View {
TextField("Enter Text", text: $post.text)
}
}
For a more detail check out Apple's Fruta and Scrumdinger samples.
I have this rather common situation where a List is displaying some items from an #ObservedObject data store as NavigationLinks.
On selecting a NavigationLink a DetailView is presented. This view has a simple Toggle conected to a #Published var on its ViewModel class.
When the DetailView appears (onAppear:) its View Model sets the published var that controls the Toggle to true and also triggers an async request that will update the main data store, causing the List in the previous screen to update too.
The problem is that when this happens (the List is reloaded from an action triggered in the Detail View) multiple instances of the DetailViewModel seem to retained and the DetailView starts receiving events from the wrong Publishers.
In the first time the Detail screen is reached the behaviour is correct (as shown in the code below), the toggle is set to true and the store is updated, however, on navigating back to the List and then again to another DetailView the toggle is set to true on appearing but this time when the code that reloads the store executes, the toggle is set back to false.
My understanding is that when the List is reloaded and a new DetailView and ViewModel are created (as the destination of the NavigationLink) the initial value from the isOn variable that controls the Toggle (which is false) is somehow triggering an update to the Toggle of the currently displayed screen.
Am I missing something here?
import SwiftUI
import Combine
class Store: ObservableObject {
static var shared: Store = .init()
#Published var items = ["one", "two"]
private init() { }
}
struct ContentView: View {
#ObservedObject var store = Store.shared
var body: some View {
NavigationView {
List(store.items, id: \.self) { item in
NavigationLink(item, destination: ItemDetailView())
}
}
}
}
struct ItemDetailView: View {
#ObservedObject var viewModel = ItemDetailViewModel()
var body: some View {
VStack {
Toggle("A toggle", isOn: $viewModel.isOn)
Text(viewModel.title)
Spacer()
} .onAppear(perform: viewModel.onAppear)
}
}
class ItemDetailViewModel: ObservableObject {
#ObservedObject var store: Store = .shared
#Published var isOn = false
var title: String {
store.items[0]
}
func onAppear() {
isOn = true
asyncOperationThatUpdatesTheStoreData()
}
private func asyncOperationThatUpdatesTheStoreData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.store.items = ["three", "four"]
}
}
}
You're controlling lifetimes and objects in a way that's not a pattern in this UI framework. The VieModel is not going to magically republish a singleton value type; it's going to instantiate with that value and then mutate its state without ever checking in with the shared instance again, unless it is rebuilt.
class Store: ObservableObject {
static var shared: Store = .init()
struct ContentView: View {
#ObservedObject var store = Store.shared
struct ItemDetailView: View {
#ObservedObject var viewModel = ItemDetailViewModel()
class ViewModel: ObservableObject {
#Published var items: [String] = Store.shared.items
There are lots of potential viable patterns. For example:
Create class RootStore: ObservableObject and house it as an #StateObject in App. The lifetime of a StateObject is the lifetime of that view hierarchy's lifetime. You can expose it (a) directly to child views by .environmentObject(store) or (b) create a container object, such as Factory that vends ViewModels and pass that via the environment without exposing the store to views, or (c) do both a and b.
If you reference the store in another class, hold a weak reference weak var store: Store?. If you keep state in that RootStore on #Published, then you can subscribe directly to that publisher or to the RootStore's own ObjectWillChangePublisher. You could also use other Combine publishers like CurrentValueSubject to achieve the same as #Published.
For code examples, see
Setting a StateObject value from child view causes NavigationView to pop all views
SwiftUI #Published Property Being Updated In DetailView
I have been reading about the property wrappers in SwiftUI and I see that they do a great job, but one thing which I really don't get is the difference between #EnvironmentObject and #ObservedObject.
From what I learned so far, I see that #EnvironmentObject is used when we have an object that is needed in various places in our app but we don't need to pass it through all of them. For example if we have hierarchy A -> B -> C -> D and the object is created at A, it is saved in the environment so that we can pass it directly from A to D, if D needs it.
If we use #ObservedObject which is created at A and needs to be passed to D, then we need to go through B and C as well.
But I still don't know how to decide which one to use.
Here are 2 example projects which I made:
struct ContentView2: View {
var order = Order2()
var body: some View {
VStack {
EditView2()
DisplayView2()
}
.environmentObject(order)
}
}
struct EditView2: View {
#EnvironmentObject var user: Order2
var body: some View {
HStack{
TextField("Fruit", text: $user.item)
}
}
}
struct DisplayView2: View {
#EnvironmentObject var user: Order2
var body: some View {
VStack{
Text(user.item)
}
}
}
class Order2: ObservableObject {
#Published var item = "Orange"
}
and
struct ContentView: View {
var order = Order()
var body: some View {
VStack {
EditView(order: order)
DisplayView(order: order)
}
}
}
struct EditView: View {
#ObservedObject var order: Order
var body: some View {
HStack{
TextField("Fruit", text: $order.item)
}
}
}
struct DisplayView: View {
#ObservedObject var order: Order
var body: some View {
VStack{
Text(order.item)
}
}
}
class Order: ObservableObject {
#Published var item = "Apple"
}
Both codes do the same update of the view. Also both ContentViews, pass an Order object. The difference is that Environment passes .environmentObject(order) and Observed passes it directly EditView(order: order). For me, both do same job, only their declaration is different, therefore I would appreciate some explanation or a better example.
As you've noticed an #ObservedObject needs to be passed from view to view. It may be better for a simple view hierarchy when you don't have too many views.
Let's assume you have the following hierarchy:
ViewA -> ViewB -> ViewC -> ViewD
Now if you want your #ObservedObject from the ViewA to be in the ViewB there's no problem with passing it directly in init.
But what if you want it in the ViewD as well? And what if you don't need it in the ViewB and ViewC?
With an #ObservedObject you'd need to manually pass it from the ViewA to the ViewB and then to the ViewC, and then to the ViewD. And you'd need to declare it in every child view.
With an #EnvironmentObject it's easy - just pass it to the top-level view:
ViewA().environmentObject(someObservableObject)
Then you only declare it in the view that uses it - this may make your code more readable.
Note
Every object in the environment (view hierarchy) can access the injected #EnvironmentObject. If you don't want this (privacy is important) you may need to pass it as an #ObservedObject instead.
My target is 2 thing:
1. to make a view depending on a view model protocol not a concrete class.
2. a sub view gets the view model from the environment instead of passing it through the view hierarchy
I've mentioned my goals so if there's a totally different way to achieve them, I'm open to suggestion.
Here's what've tried and failed of course and raised weird error:
struct ContentView: View {
var body: some View {
NavigationView {
MyView()
}
}
}
struct MyView: View {
#EnvironmentObject var viewModel: some ViewModelProtocol
var body: some View {
HStack {
TextField("Enter something...", text:$viewModel.text)
Text(viewModel.greetings)
}
}
}
//MARK:- View Model
protocol ViewModelProtocol: ObservableObject {
var greetings: String { get }
var text: String { get set }
}
class ConcreteViewModel: ViewModelProtocol {
var greetings: String { "Hello everyone..!" }
#Published var text = ""
}
//MARK:- Usage
let parent = ContentView().environmentObject(ConcreteViewModel())
Yes there is, but it's not very pretty.
You're running into issues, since the compiler can't understand how it's ever supposed to infer what type that that some protocol should be.
The reason why some works in declaring your view, is that it's inferred from the type of whatever you supply to it.
If you make your view struct take a generic viewmodel type, then you can get this up and compiling.
struct MyView<ViewModel: ViewModelProtocol>: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
Text(viewModel.greetings)
}
}
the bummer here, is that you now have to declare the type of viewmodel whenever you use this view, like so:
let test: MyView<ConcreteViewModel> = MyView()
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)
}
}
}
}
}
}
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.