Environment Objects & SwiftUI Styles
Tags: swift, swiftui, tutorial, mobile
Published: 2022-12-26 09:46:43

SwiftUI environments and styles are the two pillars of Apple's official framework (declarative structure). Despite this, when SwiftUI was first launched, using them together resulted in a guaranteed application crash.

In particular, the crash happened when we used `@EnvironmentObject` inside our style definition: when is it safe to use them together? Let's find out.

## Example
Meet `FSStyle`, a button style that expects an environment object `FSEnvironmentObject`:

```
class FSEnvironmentObject: ObservableObject {
  @Published var title = "tap me"
}

struct FSStyle: ButtonStyle {
  @EnvironmentObject var object: FSEnvironmentObject

  func makeBody(configuration: Configuration) -> some View {
    Button(object.title) { }
  }
}
```
Based on the definition, we expect everything to work as long as we inject the environment object at some point before applying the style. For example:

```
struct ContentView: View {
  @StateObject var object = FSEnvironmentObject()

  var body: some View {
    Button("tap me") {
    }
    .buttonStyle(FSStyle())
    .environmentObject(object)
  }
}
```
..and yet if we ran this code on any version of iOS prior to 14.5, it was guaranteed to fail 100% of the time with a `Fatal error: No ObservableObject of type FSEnvironmentObject found..`

It fails as soon as an environment object is used inside the `makeBody(configuration: )` method, and both the definition of the object and the execution of `makeBody(configuration: )` are irrelevant.

> There are two main ways to work around the bug: either style it to match `DynamicProperty `(many thanks to [Lin Qing Mo](https://github.com/linqingmo) for the tip!), or return a View in the `makeBody (configuration: )` method and have that View read the environment object.

Now that we have seen what the problem is, let's find out in which iOS versions and styles this error occurs.

## Test setup
We want to find out which versions of iOS are safe to use all possible styles (not just button styles). We can create a small test app and run it on all versions of iOS that support SwiftUI and get results.

Continuing with the `ButtonStyle `example, here is the complete application:

```
import UIKit
import SwiftUI

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { }

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  var object: FSEnvironmentObject = FSEnvironmentObject()

  func scene(
    _ scene: UIScene, 
    willConnectTo session: UISceneSession, 
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      let contentView = ContentView().environmentObject(object)
      window.rootViewController = UIHostingController(rootView: contentView)
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}

struct ContentView: View {
  var body: some View {
    Button("tap me") {
    }
    .buttonStyle(FSStyle())
  }
}

struct FSStyle: ButtonStyle {
  @EnvironmentObject var object: FSEnvironmentObject

  func makeBody(configuration: Configuration) -> some View {
    Button(object.title) { }
  }
}

class FSEnvironmentObject: ObservableObject {
  @Published var title = "tap me"
}
```
The application consists of a single screen that contains our test component/style.

A few notes:

we're using the UIKit lifecycle because we want to run tests on iOS 13 as well
we don't use `@StateObject` on the environment object because this property wrapper is only for iOS 14+
the only difference between testing `ButtonStyle` and other styles is defining the `FSStyle` and `ContentView` body, everything else remains the same

## CI/CD setup
We will be testing twelve versions of iOS, from iOS 13.0 to iOS 14.5, and all eight styles that support customization.

Testing each combination by hand would be quite difficult, instead we can let the CI/CD provider do all the hard work for us. Any CI/CD setup will do, here's how the different versions of Xcode/iOS were distributed for this study:

**macOS 10.14**
_iOS 13.0, Xcode 11.0
iOS 13.1, Xcode 11.1
iOS 13.2, Xcode 11.2_
**macOS 10.15**
_iOS 13.3, Xcode 11.3.1
iOS 13.4, Xcode 11.4.1
iOS 13.5, Xcode 11.5
iOS 13.6, Xcode 11.6
iOS 13.7, Xcode 11.7
iOS 14.0, Xcode 12.0.1
iOS 14.1, Xcode 12.1
iOS 14.2, Xcode 12.2
iOS 14.3, Xcode 12.3_
**macOS 11.4:**
_iOS 14.4, Xcode 12.4
iOS 14.5, Xcode 12.5_

## Results

Since the test application has only one screen that immediately displays the component under test, all that is needed to pass the test is to launch the application and not immediately crash. Here is the result:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxdnu8zkw9vxpqycd5a8.PNG)
💥 = crash, ✅ = passed. *Style available since iOS 14.

Let's summarize:

all styles except `ButtonStyle `support `@EnvironmentObject` as of iOS 14.0
since iOS 14.5 all styles including `ButtonStyle `support `@EnvironmentObject`

## Conclusions
The reason why styles + `@EnvironmentObject` was not supported from the start is likely to remain inside the SwiftUI command, however this could have been intentional:

Looking at how standard SwiftUI styles are applied, in addition to the few parameters passed through `Configuration`, most of the mutable components come from `EnvironmentValues`, such as `@Environment (\.IsEnabled)`, `@Environment (\.font)` and `@Environment (\.controlProminence)`.

Unlike `@EnvironmentObject`, `EnvironmentValues` ​​has been supported (no crash!) since iOS 13.0, so I recommend them when adding dynamics to our custom styles.

Whether this was a mistake or intentional, as an SDK vendor, it's important for us to disambiguate and clarify such scenarios for developers.

To the best of my knowledge, this has not been documented anywhere and has not been covered in any release notes: if the developers have allowed it, it is in the framework.

> Another place where this could be a problem is in View modifiers, however both `EnvironmentValues` ​​and `@EnvironmentObject` are supported (without the 💥) from iOS 13.0.