//environment object class
class AppData: ObservableObject {
#Published var studs : [StudentModel]
}
var body: some View {
VStack{
List(appData.studs,id:\.rollNo){ s in //causing error
Text("\(s.rollNo)")
NavigationLink("", destination: StudentView(s: s))
}
}.navigationBarItems(trailing:
Button(action: {
self.addStud.toggle()
}){
Image(systemName: "plus")
.renderingMode(.original)
}
.sheet(isPresented: $addStud, content: {
AddStudent()
})
)
.navigationBarTitle(Text("Students"),displayMode: .inline)
}
Fatal error: No ObservableObject of type AppData found. A View.environmentObject(_:) for AppData may be missing as an ancestor of this view.
Your sample code is missing some lines at the start of the view. By the sounds of the error message, you already have something like:
struct MyView: View {
#EnvironmentObject var appData: AppData
// ...rest of view ...
}
Alongside that code to get a reference for your object out of the environment, you also need to ensure that, somewhere further up the chain, it's put in. Your error message is telling you that that is where the problem lies – it's looking in the environment for a type of AppData object, but there's nothing in there.
Let's say you declare it the app level; it might look something like this:
#main
struct TestDemoApp: App {
// 1. Instantiate the object, using `#StateObejct` to make sure it's "owned" by the view
#StateObject var appData = AppData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appData) // 2. make it available to the hierarchy of views
}
}
}
What you'll also have to do is make sure that any views that use your environment object also have access to one in their Xcode previews. You might want to create a version of AppData that has example data inside so that your previews don't mess with live data.
extension AppData {
static var preview: AppData = ...
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(AppData.preview)
}
}
Related
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)
}
}
I get the following error: Unable to present view. Please file a bug whenever I make an asynchronous call on a view and leave the view (e.g. navigate to another view in the navigation stack) before it can make changes to the ui. Consequently, the next view in the navigation stack is unable to update its view. How can I fix this problem?
An example of the problem occurring is when I switch from view1 to view2 before my GetIoTThingIndex() call finishes and makes an update to the ui.
GetIoTThingIndex.query(device) { error in
DispatchQueue.main.async { [self] in
...
}
}
EDIT:
After doing more investigating, I found that this problem is due to the fact that I am implementing my logic in an MVVM pattern. When I moved my logic directly into the the view and called the functions and state variables inside the view, everything worked fine. It's interesting because when I started building my app with just a few pages with minimal logic and dependencies, this MVVM pattern worked fine without any bugs. However, when my project grew to 20+ pages with more logic and dependencies, the MVVM pattern causes this bug. Is this just a problem I see or has anyone seen anything like this before and have any recommendations for fixing it?
This is the way I had things with MVVM.
View
struct DeviceView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
Text(viewModel.name)
...
}
}
View Model
class DeviceViewModel: ObservableObject {
#Published var name = ""
public func updateUI() {
...
}
...
}
This is the way I have things now (which works without this bug).
View
struct DeviceView: View {
var body: some View {
Text(name)
...
}
#State var name = ""
public func updateUI() {
...
}
...
}
Are you sure this is what is happening?
I've tested the idea of navigating to another view
before the parent can make a change to its view. And all works well.
This is the code I used for the test, click on the button first, then within 3 sec click on the NavigationLink.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var thingToUpdate = ""
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("text \(thingToUpdate)")
Button("click me first") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
thingToUpdate = " is updated now"
}
}
NavigationLink(destination: Text("the detail view")) {
Text("then to DetailView")
}
}
}
}
}
Edit update using ObservableObject that works for me:
class DeviceViewModel: ObservableObject {
#Published var name = "no name"
public func updateUI() {
// simulated delay on the main thread
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.name = "success"
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("viewModel name is \(viewModel.name)")
Button("click me first") {
viewModel.updateUI()
}
NavigationLink(destination: Text("DetailView")) {
Text("then to DetailView")
}
}
}
}
}
Consider the following code:
import SwiftUI
class ViewModel: ObservableObject {
}
struct TestView: View {
#ObservedObject var vm = ViewModel()
var body: some View {
// self.sample
GeometryReader { _ in
self.sample
}
}
var sample: some View {
Text("Hello, World!")
}
}
struct Tabs : View {
#State var selection: Int = 0
var body: some View {
TabView(selection: $selection) {
TestView().tabItem {
Text("First Tab")
}
.tag(0)
Text(String(selection))
.tabItem {
Text("Second Tab")
}
.tag(1)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
There are two tabs and selection is referenced in body therefore body will be called when selection is changed.
TestView is using GeometryReader.
When I switch from "First Tab" to "Second Tab" ViewModel is created again and never dereferenced. This is unexpected.
If I switch 100 times I will have 100 ViewModels referenced from SwiftUI internals.
Though if i remove GeometryReader it works as expected.
Did someone experience it? Are there any workarounds?
I simply want this ViewModel lifetime to be bound to TestView lifetime.
UPDATE:
XCode 11.3.1 iOS 13.3
Ok, let's make the following changes in ViewModel
class ViewModel: ObservableObject {
init() {
print(">> inited") // you can put breakpoint here in Debug Preview
}
}
so now it seen that because View is value type
struct TestView: View {
#ObservedObject var vm = ViewModel() // << new instance on each creation
...
and it is originated from
var body: some View {
TabView(selection: $selection) {
TestView().tabItem { // << created on each tab switch
...
so, the solution would be to ViewModel creation out of TestView and inject outer instance either via .environmentObject or via constructor arguments.
Btw, it does not depend on GeometryReader. Tested with Xcode 11.2.1 / iOS 13.2
I'm attempting to crawl out of the proverbial Neophyte abyss here.
I'm beginning to grasp the use of #EnvironmentObject till I notice the .environmentObject() view operator in the docs.
Here's my code:
import SwiftUI
struct SecondarySwiftUI: View {
#EnvironmentObject var settings: Settings
var body: some View {
ZStack {
Color.red
Text("Chosen One: \(settings.pickerSelection.name)")
}.environmentObject(settings) //...doesn't appear to be of use.
}
func doSomething() {}
}
I tried to replace the use of the #EnvironmentObject with the .environmentObject() operator on the view.
I got a compile error for missing 'settings' def.
However, the code runs okay without the .environmentObject operator.
So my question, why have the .environmentObject operator?
Does the .environmentObject() instantiates an environmentObject versus the #environmentObject accesses the instantiated object?
Here is demo code to show variants of EnvironmentObject & .environmentObject usage (with comments inline):
struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView().environmentObject(Settings()) // environment object injection
}
}
class Settings: ObservableObject {
#Published var foo = "Foo"
}
struct RootView: View {
#EnvironmentObject var settings: Settings // declaration for request of environment object
#State private var showingSheet = false
var body: some View {
VStack {
View1() // environment object injected implicitly as it is subview
.sheet(isPresented: $showingSheet) {
View2() // sheet is different view hierarchy, so explicit injection below is a must
.environmentObject(self.settings) // !! comment this out and see run-time exception
}
Divider()
Button("Show View2") {
self.showingSheet.toggle()
}
}
}
}
struct View1: View {
#EnvironmentObject var settings: Settings // declaration for request of environment object
var body: some View {
Text("View1: \(settings.foo)")
}
}
struct View2: View {
#EnvironmentObject var settings: Settings // declaration for request of environment object
var body: some View {
Text("View2: \(settings.foo)")
}
}
So, in your code ZStack does not declare needs of environment object, so no use of .environmentObject modifier.
I am updating the state 'lessonVisible' in my parent view "LessonOverview" from my child component "LessonView"
The code its self if working as expected and the state is updated in the parent. However the code will not work with the preview.
I am getting the error below on line 45 in the preview provider struct:
Cannot convert value of type 'visibleLessonStruct' to expected
argument type 'Binding'
on the line:
LessonView(lessonVisible: visibleLessonStruct(lessonType:
.Reading)).previewDevice(PreviewDevice(rawValue: "iPhone XS"))
How do I resolve this issue?
import Foundation
import SwiftUI
struct visibleLessonStruct{
var lessonType: LessonType
}
enum LessonType{
case Reading, Listening, Practice
}
struct LessonView: View {
#Binding var lessonVisible : visibleLessonStruct!
var body: some View {
GeometryReader { geometry in
VStack{
HStack{
Spacer()
Button(action: {
print("Close button tapped!")
self.lessonVisible = nil
}) {
Image("cross").renderingMode(.original).resizable().frame(width: 40, height: 40)
}
}.padding(10).padding(.trailing, 10)
Spacer()
}.background(Color.white).statusBar(hidden: true)
}
}
}
struct LessonView_Previews: PreviewProvider {
static var previews: some View {
LessonView(lessonVisible: visibleLessonStruct(lessonType: .Reading)).previewDevice(PreviewDevice(rawValue: "iPhone XS"))
}
}
try wrapping it inside a .constant like this:
struct LessonView_Previews: PreviewProvider {
static var previews: some View {
LessonView(lessonVisible: .constant(visibleLessonStruct(lessonType: .Reading))).previewDevice(PreviewDevice(rawValue: "iPhone XS"))
}
}
constants ares used in previews to mimic the behavior of Bindable and State variables.