SwiftUI - Can't find EnvironmentObject in UITests - swiftui

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.

Related

How to access a global environment object in a class?

I have a class that needs to update a global environment object. I can pass that environment object between my structs all day, but how do I allow a class object to access the same variable?
import SwiftUI
class Global: ObservableObject
{
#Published var num = 10
}
class MyClass:ObservableObject
{
#Published var mode = 1
#EnvironmentObject var global: Global
func updateMode()
{
self.mode += 1
global.num += 1
}
}
#main
struct MyApp: App
{
let settings = Global()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}
struct ContentView: View
{
#EnvironmentObject var global: Global
#ObservedObject var myClass = MyClass()
var body: some View
{
VStack
{
Text("Setting \(global.num)")
Text("Mode \(myClass.mode)")
Button("Click Me", action: {myClass.updateMode()})
}
.padding()
}
}
The following code gives an error:
Fatal Error: No ObservableObject of type Global found. A
View.environmentObject(_:) for Global maybe missing an ancestor of
this view.
I could pass the global object into myClass.updateMode, but then it doesn't seem very global at that point? I would have thought there must be a better way.
A possible approach is to make it shared (and don't use #EnvironmentObject anywhere outside SwiftUI view - it is not designed for that):
class Global: ObservableObject
{
static let shared = Global()
#Published var num = 10
}
class MyClass:ObservableObject
{
#Published var mode = 1
let global = Global.shared // << here !!
// ...
}
#main
struct MyApp: App
{
#StateObject var settings = Global.shared // << same !!
// ...
}

How do I access my model (or other state in the App struct) from a ComplicationController when using SwiftUI lifecycle?

Given a SwiftUI Watch App:
#main
struct SomeApp: App {
#StateObject var model = SomeModel()
#SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
.environmentObject(model)
}
WKNotificationScene(controller: NotificationController.self, category: "myCategory")
}
}
How do I gain access to model in my ComplicationController? Tried using EnvironmentObject as below without success.
class ComplicationController: NSObject, CLKComplicationDataSource {
#EnvironmentObject var model: SomeModel
...
Deeper question is what is the relative lifecycles of the App struct and the ComplicationsController. I have a heavy model and I only want to instantiate it once. Does it just belong as a global variable?
As SomeModel is a single state object of the app, then you can just make it shared and access explicitly, like shown below
class SomeModel: ObservableObject {
static let shared = SomeModel()
// ... other code
so
#main
struct SomeApp: App {
#StateObject var model = SomeModel.shared // << here
...
and
class ComplicationController: NSObject, CLKComplicationDataSource {
var model = SomeModel.shared // << here
...
Note: EnvironmentObject works only in SwiftUI views, so in ComplicationController it is useless (or even harmful)

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

EnvironmentObject not found for child Shape in 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)
}
}

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.