SwiftUI EnvironmentObject not available in View initializer? - swiftui

I passed the environmentObject appSettings into my view successfully. I can use it to modify my font and the picker in my View. But if I try to access an environmentObject published variable in the view init() it crashes with:
Thread 1: Fatal error: No ObservableObject of type AppSettings found.
A View.environmentObject(_:) for AppSettings may be missing as an ancestor of this view.
Are there special rules about using an environmentObject in a custom SwiftUI View initializer?
Here's the start of my view code. The environmentObject is appSettings. If I comment out line 2 in my initializer and uncomment line 3 the app works. Note that I use "appSettings.interfaces" successfully later in my Picker.
struct CaptureFilterView: View {
#State var etherCapture: EtherCapture? = nil
#EnvironmentObject var appSettings: AppSettings
#Binding var frames: [Frame]
#State var captureFilter: String = ""
#State var error: String = ""
#State var numberPackets = 10
#State var interface: String = ""
init(frames: Binding<[Frame]>) {
self._frames = frames
self.interface = appSettings.interfaces.first ?? "en0" //CRASH HERE
//self.interface = "en0" //uncomment this and comment line above to make app "work"
}
var body: some View {
HStack() {
...
Picker(selection: $interface, label: Text("")) {
ForEach(appSettings.interfaces, id: \.self) { interfaceName in
Text(interfaceName).tag(interfaceName)
}
}
Here's where I create my top-level content view in my AppDelegate.swift
let contentView = ContentView(showCapture: true).environmentObject(appSettings)
And just to be sure I also pass on the environmentObject when creating my CaptureFilterView in my top level ContentView. This is not necessary and does not change the behavior.
if showCapture { CaptureFilterView(frames: self.$frames).environmentObject(appSettings) }
For reference here is the top of my appSettings:
class AppSettings: ObservableObject {
#Published var font: Font
#Published var interfaces: [String]

SwiftUI EnvironmentObject not available in View initializer?
Yes, SwiftUI EnvironmentObject not available in View initializer. Why? It is simple - it is injected after object initialiazation.
Let's consider how it is done on example of above ContentView:
let contentView = ContentView(showCapture: true).environmentObject(appSettings)
so what's going on here? Here
instantiation & initialisation of value for the type ContentView
let newInstance = ContentView.init(showCapture: true)
calling function func environmentObject() on newInstance injected appSetting property
let contentView = newInstance.environmentObject(appSettings)

Related

referenced binding param not triggering animation

I have a custom view
struct CustomView: View {
#Binding var text: String
...
var body: some View {
VStack {
...
Text(SomeText)
.offset(y: text.isEmpty ? 0 : -25)
.scaleEffect(text.isEmpty ? 1 : 0.5, anchor: .leading)
.animation(.spring(), value: text.isEmpty)
...
}
the scale effect and offset animation never trigger if text is referenced from an object.
For example, I have a ViewModel such as
class SomeViewModel: ObservableObject {
#Published var text: String = ""
}
And the parent View such as
struct ParentView: View {
#State private var vm = SomeViewModel()
#State private var text = "" //this one works!
...
var body: some View {
...
CustomView(..., text: $vm.text) // no animation, but the value of v.name is updated
CustomView(..., text: $text)) // everything works, including animation
For SwiftUI to properly update based on changes of an ObservableObject, you will need to use a different property wrapper. Usually this is either #ObservedObject or #StateObject dependent on the context you are using it in.
Try using #StateObject if this is where you are first initialising the class.
StateObject Documentation

SwiftUI connecting #Binding to #Published directly in ChildView

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

Is this the right way for using #ObservedObject and #EnvironmentObject?

Cow you give me some confirmation about my understanding about #ObservedObject and #EnvironmentObject?
In my mind, using an #ObservedObject is useful when we send data "in line" between views that are sequenced, just like in "prepare for" in UIKit while using #EnvironmentObject is more like "singleton" in UIKit. My question is, is my code making the right use of these two teniques? Is this the way are applied in real development?
my model used as brain for funcions (IE urls sessions, other data manipulations)
class ModelClass_ViaObservedObject: ObservableObject {
#Published var isOn: Bool = true
}
class ModelClass_ViaEnvironment: ObservableObject {
#Published var message: String = "default"
}
my main view
struct ContentView: View {
//way to send data in views step by step
#StateObject var modelClass_ViaObservedObject = ModelClass_ViaObservedObject()
//way to share data more or less like a singleton
#StateObject var modelClass_ViaEnvironment = ModelClass_ViaEnvironment()
var myBackgroundColorView: Color {
if modelClass_ViaObservedObject.isOn {
return Color.green
} else {
return Color.red
}
}
var body: some View {
NavigationView {
ZStack {
myBackgroundColorView
VStack {
NavigationLink(destination:
SecondView(modelClass_viaObservedObject: modelClass_ViaObservedObject)
) {
Text("Go to secondary view")
.padding()
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.black, lineWidth: 1)
)
}
Text("text received from second view: \(modelClass_ViaEnvironment.message)")
}
}
.navigationTitle("Titolo")
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(modelClass_ViaEnvironment)
}
}
my second view
struct SecondView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var modelClass_viaObservedObject: ModelClass_ViaObservedObject
//global data in environment, not sent step by step view by view
#EnvironmentObject var modelClass_ViaEnvironment: ModelClass_ViaEnvironment
var body: some View {
VStack(spacing: 5) {
Text("Second View")
Button("change bool for everyone") {
modelClass_viaObservedObject.isOn.toggle()
dismiss()
}
TextField("send back", text: $modelClass_ViaEnvironment.message)
Text(modelClass_ViaEnvironment.message)
}
}
}
No, we use #State for view data like if a toggle isOn, which can either be a single value itself or a custom struct containing multiple values and mutating funcs. We pass it down the View hierarchy by declaring a let in the child View or use #Binding var if we need write access. Regardless of if we declare it let or #Binding whenever a different value is passed in to the child View's init, SwiftUI will call body automatically (as long as it is actually accessed in body that is).
#StateObject is for when a single value or a custom struct won't do and we need a reference type instead for view data, i.e. if persisting or syncing data (not using the new async/await though because we use .task for that). The object is init before body is called (usually before it is about to appear) and deinit when the View is no longer needed (usually after it disappears).
#EnvironmentObject is usually for the store object that holds model structs in #Published properties and is responsible for saving or syncing,. The difference is the model data is not tied to any particular View, like #State and #StateObject are for view data. This object is usually a singleton, one for the app and one with sample data for when previewing, because it should never be deinit. The advantage of #EnvironmentObject over #ObservedObject is we don't need to pass it down through each View as a let that don't need the object when we only need it further down the hierarchy. Note the reason it has to be passed down as a let and not #ObservedObject is then body would be needlessly called in the intermediate Views because SwiftUI's dependency tracking doesn't work for objects only value types.
Here is some sample code:
struct MyConfig {
var isOn = false
var message = ""
mutating func reset() {
isOn = false
message = ""
}
}
struct MyView: View {
#State var config = MyConfig() // grouping vars into their struct makes use of value semantics to track changes (a change to any of its properties is detected as a change to the struct itself) and offers testability.
var body: some View {
HStack {
ViewThatOnlyReads(config: config)
ViewThatWrites(config: $config)
}
}
}
struct ViewThatOnlyReads: View {
let config: MyConfig
var body: some View {
Text(config.isOn ? "It's on" : "It's off")
}
}
struct ViewThatWrites: View {
#Binding var config: MyConfig
var body: some View {
Toggle("Is On", isOn: $config.isOn)
}
}

EnvironmentObject causes unrelated ObservedObject to reset

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.

SwiftUI pass Binding by ref to a child ViewModel

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.