EnvironmentObject not found for child Shape in SwiftUI - swiftui

I am encountering the following SwiftUI error with #EnvironmentObject when used with a custom Shape, :
Fatal error: No ObservableObject of type MyObject found. A View.environmentObject(_:) for MyObject may be missing as an ancestor of this view.: file SwiftUI, line 0
It only happens when I use any Shape method that returns a new copy of the instance like stroke().
Here is a Swift playground example to reproduce:
import SwiftUI
import PlaygroundSupport
class MyObject: ObservableObject {
#Published var size: Int = 100
}
struct MyShape: Shape {
#EnvironmentObject var envObj: MyObject
func path(in rect: CGRect) -> Path {
let path = Path { path in
path.addRect(CGRect(x: 0, y: 0,
width: envObj.size, height: envObj.size))
}
return path
}
}
struct MyView: View {
var body: some View {
MyShape().stroke(Color.red) // FAIL: no ObservableObject found
// MyShape() // OK: it works
}
}
let view = MyView().environmentObject(MyObject())
PlaygroundPage.current.setLiveView(view)
As it looks like environment field is not copied, I've also tried to do it explicitly like this:
struct MyView: View {
#EnvironmentObject var envObj: MyObject
var body: some View {
MyShape().stroke(Color.red).environmentObject(self.envObj)
}
}
It still fails. As a SwiftUI beginner, I don't know whether this is the expected behavior, not inheriting the view hierarchy environment, and how to handle it - other than not using the environment.
Any idea?

The problem actually is that .stroke is called right after constructor, so before environmentObject injected (you can test that it works if you comment out stroke). But .stroke cannot be added after environment object injected, because .stroke is Shape-only modifier.
The solution is to inject dependency during construction as below. Tested with Xcode 11.4 / iOS 13.4
struct MyShape: Shape {
#ObservedObject var envObj: MyObject
...
}
struct MyView: View {
#EnvironmentObject var envObj: MyObject
var body: some View {
MyShape(envObj: self.envObj).stroke(Color.red)
}
}

Related

SKScene with bindings in a SpriteView

I've created a SpriteKit Scene file and the corresponding SKScene object. That scene object includes an #Binding property.
When I use SpriteView in my SwiftUI file, I can't figure out how to initialize the scene so that it loads from the sks file and also assigns a binding.
I'm wanting something like this:
class MyScene: SKScene {
#Binding var foo: CGFloat
init(foo: Binding<CGFloat>) {
_foo = foo
super.init(fileNamed: "MyScene")
}
}
That doesn't work though because init(fileNamed:) is a convenience initializer, not a designated initializer.
A possible workaround using ObservableObject to subscribe to foo. I pass a fileName so I have an excuse to invoke my convenience initializer.
class MyScene: SKScene, ObservableObject {
#Published var foo: CGFloat = 0
convenience init(fileName: String) {
self.init(fileNamed: fileName)!
}
}
And then hold a reference to it on your view (or elsewhere).
struct MyView: View {
#ObservedObject var scene = MyScene(fileName: "MyScene")
var body: some View {
VStack {
FooView($scene.foo)
SpriteView(scene: scene)
}
}
}

No ObservableObject of type XXX found just inside a notification observer

I have a class like this:
class GlobalVariables: ObservableObject {
#Published var aaa = AAA()
#Published var bbb = BBB()
#Published var ccc = CCC()
}
When I want to access that, I add this to a view:
#EnvironmentObject var globalVariables : GlobalVariables
and that's it.
So, I did that to my view called MyView.
and I am happy. I can access globalVariables almost anywhere inside MyView.
But, and there is always a but, MyView contains this method:
func initNotification() {
let gv = globalVariables // 1
NotificationCenter.default
.addObserver(forName: .runOnDetectedObject,
object: nil,
queue: OperationQueue.main,
using: {notification in
globalVariables.aaa(object:myObj)) // 2
})
}
//1 and //2 compile fine, but when I run, both lines crash with
Thread 1: Fatal error: No ObservableObject of type GlobalVariables found. A View.environmentObject(_:) for GlobalVariables may be missing as an ancestor of this view.
I have this on MyApp.swift
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(GlobalVariables())
}
}
The view I am having the problem is not ContentView()
Why?
Without a Minimal Reproducible Example it is impossible to help you troubleshoot.
But, you have to pass the EnvironmentObject down.
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var globalVariables : GlobalVariables
var body: some View {
MyView().environmentObject(globalVariables)
}
}
It usually only works for about 2-3 layers. If you go any deeper than that it is pretty buggy.

SwiftUI - Can't find EnvironmentObject in UITests

I'm trying to use #EnvironmentObject in my UITests, but I get an error at runtime.
Thread 1: Fatal error: No ObservableObject of type Person found. A View.environmentObject(_:) for Person may be missing as an ancestor of this view.
Doesn't Work
The following code throws the error mentioned above when running the UITest.
import SwiftUI
#main
struct RootView: App {
var body: some Scene {
WindowGroup {
AppView()
.environmentObject(Person())
}
}
}
class Person: ObservableObject {
#Published var name = "Lex"
}
UITest
import XCTest
import SwiftUI
class AppTests: BaseUITest {
#EnvironmentObject var person: Person
func testPerson() {
assert(person.name == "Lex")
}
}
// For your info.
class BaseUITest: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launch()
}
}
Current alternative: global variable
The following code works just fine.
import SwiftUI
var person = Person()
#main
struct RootView: App {
var body: some Scene {
WindowGroup {
AppView()
.environmentObject(person)
}
}
}
import XCTest
class AppTests: BaseUITest {
func testPerson() {
assert(person.name == "Lex")
}
}
My Goal
My goal is to step away from the usage of global variables, as those are a code smell. I'm trying to resolve my code smells.
The answer I'm looking for can be:
A solution to allow the usage of #EnvironmentObject in my UITests!
An explanation as to why this is not possible and/or an alternative solution to both global variables and #EnvironmentObject that works, looks clean and is not a different code smell.

Is there a way decouple views from view models like the following?

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

What's the purpose of .environmentObject() view operator vs #EnvironmentObject?

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.