I'm not sure if this is an antipattern in this brave new SwiftUI world we live in, but essentially I have an #EnvironmentObject with some basic user information saved in it that my views can call.
I also have an #ObservedObject that owns some data required for this view.
When the view appears, I want to use that #EnvironmentObject to initialize the #ObservedObject:
struct MyCoolView: View {
#EnvironmentObject userData: UserData
#ObservedObject var viewObject: ViewObject = ViewObject(id: self.userData.UID)
var body: some View {
Text("\(self.viewObject.myCoolProperty)")
}
}
Unfortunately I can't call self on the environment variable until after initialization:
"Cannot use instance member 'userData' within property initializer; property initializers run before 'self' is available."
I can see a few possible routes forward, but they all feel like hacks. How should I approach this?
Here is the approach (the simplest IMO):
struct MyCoolView: View {
#EnvironmentObject var userData: UserData
var body: some View {
MyCoolInternalView(ViewObject(id: self.userData.UID))
}
}
struct MyCoolInternalView: View {
#EnvironmentObject var userData: UserData
#ObservedObject var viewObject: ViewObject
init(_ viewObject: ViewObject) {
self.viewObject = viewObject
}
var body: some View {
Text("\(self.viewObject.myCoolProperty)")
}
}
Related
We needed to bind to the #Published in a ParentView's #StateObject being used as a ViewModel. I told my coworker we needed to pass in the entire ViewModel to an #ObservedObject in the ChildView, since everything I've read about SwiftUI describes the parent-child data relationships like this:
Parent View
Child View
#State
#Binding
#StateObject
#ObservedObject
#Published (within the #StateObject)
DOESN'T EXIST
He insisted we could just pass in the #Published alone to the ChildView's #Binding and it would work. Lo and behold, he was right. In the example code below is the documented way to use #Binding, along with passing the entire #StateObject down into an #ObservedObject and binding to the #Published directly (which I thought was the only way you could/should bind to an #Published), and passing the #Published directly into the #Binding, which surprisingly does work. You can see in the sample pic that when you type in any of the 3 TextField's, the updates appear immediately in the parent view, proving that the #Binding works with all 3 approaches.
Is this a fluke about binding to the #Published directly? Or an undocumented-but-valid approach? Why isn't this taught as a standard approach?
import SwiftUI
class MyViewModel: ObservableObject {
#Published var publishedStr1 = "I am publishedStr1"
#Published var publishedStr2 = "I am publishedStr2"
}
struct ParentView: View {
#State var stateStr = "I am stateStr"
#StateObject var myViewModel = MyViewModel()
var body: some View {
VStack {
ChildView(stateStr: $stateStr,
myViewModel: myViewModel,
// Why don't I ever see examples using this technique
// of passing the #Published directly to an #Binding?
// It clearly works.
publishedStr2: $myViewModel.publishedStr2)
Text(stateStr)
Text(myViewModel.publishedStr1)
Text(myViewModel.publishedStr2)
}
}
}
struct ChildView: View {
#Binding var stateStr: String
#ObservedObject var myViewModel: MyViewModel
#Binding var publishedStr2: String
var body: some View {
VStack {
TextField("Binding to State", text: $stateStr)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Binding to ObservedObject",
text: $myViewModel.publishedStr1)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Binding to ObservedObject's Published",
text: $publishedStr2)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
#StateObject is only when we need a reference type for state. In your case you don't so just use #State, e.g.
struct ParentViewConfig {
var publishedStr1 = "I am publishedStr1"
var publishedStr2 = "I am publishedStr2"
var stateStr = "I am stateStr"
mutating func someFunc() {
}
}
struct ParentView: View {
#State var config = ParentViewConfig()
I am not quite sure I understand what is going on here as I am experimenting with an EnvironmentObject in SwiftUI.
I recreated my problem with a small example below, but to summarize: I have a ContentView, ContentViewModel, and a StateController. The ContentView holds a TextField that binds with the ContentViewModel. This works as expected. However, if I update a value in the StateController (which to me should be completely unrelated to the ContentViewModel) the text in the TextField is rest.
Can someone explain to me why this is happening, and how you could update a state on an EnvironmentObject without having SwiftUI redraw unrelated parts?
App.swift
#main
struct EnvironmentTestApp: App {
#ObservedObject var stateController = StateController()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(stateController)
}
}
}
ContentView.swift
struct ContentView: View {
#ObservedObject private var viewModel = ContentViewModel()
#EnvironmentObject private var stateController: StateController
var body: some View {
HStack {
TextField("Username", text: $viewModel.username)
Button("Update state") {
stateController.validated = true
}
}
}
}
ContentViewModel.swift
class ContentViewModel: ObservableObject {
#Published var username = ""
}
StateController.swift
class StateController: ObservableObject {
#Published var validated = false
}
Like lorem-ipsum pointed out, you should use #StateObject.
A good rule of thumb is to use #StateObject every time you init a viewModel, but use #ObservedObject when you are passing in a viewModel that has already been init.
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.
Using a wrapper allows us to pass EnvironmentObject down into ObservedObject. Nice approach..
But what if you want to manipulate the userData inside ViewObject without an entirely new ViewObject being created every time?
In my app entire view is recreated after I change EnvironmentObject and i don't know how to avoid this.
struct MyCoolView: View {
#EnvironmentObject var userData: UserData
var body: some View {
MyCoolInternalView(ViewObject(id: self.userData.UID))
}
}
struct MyCoolInternalView: View {
#EnvironmentObject var userData: UserData
#ObservedObject var viewObject: ViewObject
init(_ viewObject: ViewObject) {
self.viewObject = viewObject
}
var body: some View {
Text("\(self.viewObject.myCoolProperty)")
}
}
But what if you want to manipulate the userData inside ViewObject without an entirely new ViewObject being created every time?
Here is a demo of possible solution - make viewObject as StateObject (this will make it persistent through view refresh) and inject userData into it
Tested with Xcode 12.1 / iOS 14.1
class UserData: ObservableObject {
#Published var UID = "1"
}
class ViewObject: ObservableObject {
var userData: UserData?
#Published var myCoolProperty = "Hello"
}
struct MyCoolView: View {
#EnvironmentObject var userData: UserData
var body: some View {
MyCoolInternalView(self.userData)
}
}
struct MyCoolInternalView: View {
#StateObject private var viewObject = ViewObject()
init(_ userData: UserData) {
self.viewObject.userData = userData
}
var body: some View {
Text("\(self.viewObject.myCoolProperty)")
}
}
I'd like to update an UI element on an overview view when data on another view is changed.
I looked into #EnvironmentalObject and #Binding. However, an update to either object does not appear to force a view reload. Only changes to #State force renders.
Also, in the case described below, the ChangeView is not a child of OverviewView. Therefore #Binding is not an option anyway.
Data.swift
struct ExampleData : Hashable {
var id: UUID
var name: String
}
var globalVar: ExampleData = ExampleData(id: UUID(), name:"")
OverviewView.swift
struct OverviewView: View {
#State private var data: ExampleData = globalVar
var body: some View {
Text(data.name)
}
}
ChangeView.swift
struct ChangeView: View {
#State private var data: ExampleData = globalVar
var body: some View {
TextField("Name", text: $data.name, onEditingChanged: { _ in
globalVar = data }, onCommit: { globalVar = data })
}
}
Changes within the ChangeView TextField will update the globalVar. However, this will not update the Text on the OverviewView when switching back to the view.
I am aware that using global variables is "ugly" coding. How do I handle data that will be used in a multitude of unrelated views?
Please advise on how to better handle such a situation.
OverviewView and ChangeView hold different copies of the ExampleData struct in their data variables (When assigning a struct to another variable, you're effectively copying it instead of referencing it like an object.) so changing one won't affect the other.
#EnvironmentObject suits your requirements.
Here's an example:
Since, we're using #EnvironmentObject, you need to either convert ExampleData to
a class, or use a class to store it. I'll use the latter.
class ExampleDataHolder: ObservableObject {
#Published var data: ExampleData = ExampleData(id: UUID(), name:"")
}
struct CommonAncestorOfTheViews: View {
var body: some View {
CommonAncestorView()
.environmentObject(ExampleDataHolder())
}
}
struct OverviewView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
Text(dataHolder.data.name)
}
}
struct ChangeView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
TextField("Name", text: $dataHolder.data.name)
}
}