Dump SwiftUI Environment efficiently

Dump SwiftUI Environment efficiently

Motivation

This blog post will explain how to print environment values efficiently for debugging purposes. Therefore, you can inspect which actual value is used on a specific view hierarchy level.

A quick recap about SwiftUI Environment

SwiftUI uses the environment to pass values for pre-defined types down the view hierarchy.

By using @Environment property wrapper, we can read and subscribe on changes for the selected value.

struct ContentView: View {
  @Environment(\.sizeCategory) var sizeCategory
  @Environment(\.horizontalSizeClass) var horizontalSizeClass
  @Environment(\.scenePhase) var scenePhase

  var body: some View { ... }
}

We can also create your custom environment values.

private struct MagicColorKey: EnvironmentKey {
  static let defaultValue = Color(.secondarySystemBackground)
}

extension EnvironmentValues {
  var magicColor: Color {
    get { self[MagicColorKey.self] }
    set { self[MagicColorKey.self] = newValue }
  }
}

We are using the environment view modifier to inject our custom environment value into the SwiftUI environment. Values can be overridden on a lower view hierarchy level. Here is an example:

struct ContentView: View {
    var body: some View {
        VStack {
            HStack {
                Text("What is the magic color for this leaf node?")
            }
            .environment(\.magicColor, .blue)
        }
        .environment(\.magicColor, .red)
    }
}

Dump a simple value

The low-key debugging option is to use the transformEnvironment function in combination with the dump function from the Swift Standard Library.

Text("What is the magic color for this leaf node?")
  .transformEnvironment(\.magicColor) { dump($0) }

Dump will print the given object’s contents using its mirror to the standard output. The printed result in the Xcode debug console shows, that on this view hierarchy level, the magic color is blue (as expected).

▿ blue
  ▿ provider: SwiftUI.(unknown context at $7fff5dc005b0).ColorBox<SwiftUI.SystemColorType> #0
    - super: SwiftUI.AnyColorBox
      - super: SwiftUI.AnyShapeStyleBox
    - base: SwiftUI.SystemColorType.blue

Dump all values

Chris Eidhoff shares a code snippet in his article SwiftUI: Setting Environment Values to inspect the current environment for a view using the following utility wrapper view:

struct DumpingEnvironment<V: View>: View {
    @Environment(\.self) var env
    let content: V
    var body: some View {
        dump(env)
        return content
    }
}

For example, we could dump the environment for a leaf node of the view above:

struct ContentView: View {
    var body: some View {
        VStack {
            HStack {
                DumpingEnvironment(content: Text("What is the magic color for this leaf node?"))
            }
            .environment(\.magicColor, .blue)
        }
        .environment(\.magicColor, .red)
    }
}

This solution works fine and can easily be copied and used in other projects. But when we run the code, it prints a colossal tree of a serialized property list.

  ▿ _plist: [EnvironmentPropertyKey<MagicColorKey> = blue, EnvironmentPropertyKey<MagicColorKey> = red, EnvironmentPropertyKey<RedactionReasonsKey> = RedactionReasons(rawValue: 2), EnvironmentPropertyKey<SceneStorageValuesKey> = Optional(SwiftUI.WeakBox<SwiftUI.SceneStorageValues>(base: Optional(SwiftUI.SceneStorageValues))), EnvironmentPropertyKey<StoreKey<SceneBridge>> = Optional(SceneBridge: rootViewController = Optional(<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7f915800c530>)), EnvironmentPropertyKey<AppNavigationAuthorityKey> = Optional(SwiftUI.WeakBox<SwiftUI.AppNavigationAuthority>(base: Optional(SwiftUI.AppNavigationAuthority))), EnvironmentPropertyKey<EditModeKey> = Optional(SwiftUI.Binding<SwiftUI.EditMode>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.StoredLocation<SwiftUI.EditMode>, _value: SwiftUI.EditMode.inactive)), EnvironmentPropertyKey<InputAccessoryKey> = WeakBox<InputAccessoryGenerator<SwiftUIToolbar>>(base: nil), EnvironmentPropertyKey<CanTakeFocusKey> = false, EnvironmentPropertyKey<IsPlatformFocusSystemEnabled> = false, EnvironmentPropertyKey<IsFocusedKey> = false, EnvironmentPropertyKey<FocusBridgeKey> = WeakBox<FocusBridge>(base: Optional(SwiftUI.FocusBridge)), EnvironmentPropertyKey<AllowedBehaviorsKey> = HostingControllerAllowedBehaviors(rawValue: 16), EnvironmentPropertyKey<ActiveContextMenuKey> = ViewIdentity(seed: 0), EnvironmentPropertyKey<Key> = Optional(SwiftUI.NavigationAuthority(host: Optional(<_TtGC7SwiftUI14_UIHostingViewGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7f91581066f0; frame = (0 0; 390 844); autoresize = W+H; gestureRecognizers = <NSArray: 0x600003b52fa0>; layer = <CALayer: 0x6000035073c0>>))), EnvironmentPropertyKey<PresentationModeKey> = Binding<PresentationMode>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.LocationBox<SwiftUI.FunctionalLocation<SwiftUI.PresentationMode>>, _value: SwiftUI.PresentationMode(isPresented: false)), EnvironmentPropertyKey<AccessibilityRequestFocusKey> = AccessibilityRequestFocusAction(onAccessibilityFocus: nil), EnvironmentPropertyKey<AccentColorKey> = Optional(#007AFFFF), EnvironmentPropertyKey<HostingViewOpenURLActionKey> = Optional(SwiftUI.OpenURLAction(handler: SwiftUI.OpenURLAction.Handler.system((Function)), isDefault: false)), EnvironmentPropertyKey<UndoManagerKey> = Optional(<NSUndoManager: 0x60000161a620>), EnvironmentPropertyKey<SceneSessionKey> = Optional(SwiftUI.WeakBox<__C.UISceneSession>(base: Optional(<UISceneSession: 0x600002066140; scene = <UIWindowScene: 0x7f9158008d60; sceneIdentifier: "sceneID:us.eidinger.DateExample-F8369E9A-67DA-48FC-BAB6-EC1A5D8D56CC">; role = UIWindowSceneSessionRoleApplication; sceneConfiguration = <UISceneConfiguration: 0x600002066240>; persistentIdentifier = F8369E9A-67DA-48FC-BAB6-EC1A5D8D56CC; userInfo = <(null): 0x0>))), EnvironmentPropertyKey<SystemColorSchemeKey> = light, EnvironmentPropertyKey<ExplicitPreferredColorSchemeKey> = nil, EnvironmentPropertyKey<AccessibilityLargeContentViewerKey> = false, EnvironmentPropertyKey<EnabledTechnologiesKey> = AccessibilityTechnologies(technologySet: SwiftUI.(unknown context at $7fff5dc3e438).AccessibilityTechnologySet(rawValue: 0)), EnvironmentPropertyKey<AccessibilityButtonShapesKey> = false, EnvironmentPropertyKey<AccessibilityPrefersCrossFadeTransitionsKey> = false, EnvironmentPropertyKey<AccessibilityInvertColorsKey> = false, EnvironmentPropertyKey<AccessibilityReduceMotionKey> = false, EnvironmentPropertyKey<AccessibilityReduceTransparencyKey> = false, EnvironmentPropertyKey<AccessibilityDifferentiateWithoutColorKey> = false, EnvironmentPropertyKey<BackgroundInfoKey> = BackgroundInfo(layer: 0, groupCount: 0), EnvironmentPropertyKey<VerticalUserInterfaceSizeClassKey> = Optional(SwiftUI.UserInterfaceSizeClass.regular), EnvironmentPropertyKey<HorizontalUserInterfaceSizeClassKey> = Optional(SwiftUI.UserInterfaceSizeClass.compact), EnvironmentPropertyKey<DisplayScaleKey> = 3.0, EnvironmentPropertyKey<ColorSchemeKey> = light, EnvironmentPropertyKey<ColorSchemeContrastKey> = standard, EnvironmentPropertyKey<DisplayGamutKey> = displayP3, EnvironmentPropertyKey<LegibilityWeightKey> = Optional(SwiftUI.LegibilityWeight.regular), EnvironmentPropertyKey<DynamicTypeSizeKey> = large, EnvironmentPropertyKey<LayoutDirectionKey> = leftToRight, EnvironmentPropertyKey<ScenePhaseKey> = inactive, EnvironmentPropertyKey<TimeZoneKey> = America/Los_Angeles (fixed (equal to current)), EnvironmentPropertyKey<CalendarKey> = gregorian (current), EnvironmentPropertyKey<LocaleKey> = en_US (current)]
    ▿ elements: Optional(EnvironmentPropertyKey<MagicColorKey> = blue)
      ▿ some: EnvironmentPropertyKey<MagicColorKey> = blue #0
        ▿ super: SwiftUI.PropertyList.Element
          - keyType: SwiftUI.(unknown context at $7fff5dca9ff0).EnvironmentPropertyKey<DateExample.(unknown context at $1011eaf88).MagicColorKey> #1
          - before: nil
          ▿ after: Optional(EnvironmentPropertyKey<MagicColorKey> = red)
            ▿ some: EnvironmentPropertyKey<MagicColorKey> = red #2
              ▿ super: SwiftUI.PropertyList.Element
                - keyType: SwiftUI.(unknown context at $7fff5dca9ff0).EnvironmentPropertyKey<DateExample.(unknown context at $1011eaf88).MagicColorKey> #1
                - before: nil
                ▿ after: Optional(EnvironmentPropertyKey<RedactionReasonsKey> = RedactionReasons(rawValue: 2))
                  ▿ some: EnvironmentPropertyKey<RedactionReasonsKey> = RedactionReasons(rawValue: 2) #3
                    ▿ ...

Too huge! The complete environment with all its values across all hierarchy levels is dumped. We need to read the output from top to bottom and ignore subsequent entries for the same EnvironmentPropertyKey (as the top-level value for a EnvironmentPropertyKey is the used one)

In most cases a more efficient way is to use a custom function to print out the environment values.

My initial approach was to split(separator: ",") the description of EnvironmentValues as EnvironmentValues conforms to CustomStringConvertible. However, this does not work correctly for value descriptions containing a comma and also the printout still contains multiple entries for the same key.

I ended up with the following "good enough" implementation

Now ~ 50 lines (instead of ~ 1160 lines !!!) are printed.

Example

--- Environment Values - BEGIN ---
EnvironmentPropertyKey<MagicColorKey> = blue
EnvironmentPropertyKey<SceneStorageValuesKey> = Optional(SwiftUI.WeakBox<SwiftUI.SceneStorageValues>(base: Optional(SwiftUI.SceneStorageValues)))
EnvironmentPropertyKey<StoreKey<SceneBridge>> = Optional(SceneBridge: rootViewController = Optional(<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7f97bcb0aef0>))
EnvironmentPropertyKey<AppNavigationAuthorityKey> = Optional(SwiftUI.WeakBox<SwiftUI.AppNavigationAuthority>(base: Optional(SwiftUI.AppNavigationAuthority)))
EnvironmentPropertyKey<EditModeKey> = Optional(SwiftUI.Binding<SwiftUI.EditMode>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.StoredLocation<SwiftUI.EditMode>, _value: SwiftUI.EditMode.inactive))
EnvironmentPropertyKey<InputAccessoryKey> = WeakBox<InputAccessoryGenerator<SwiftUIToolbar>>(base: nil)
EnvironmentPropertyKey<CanTakeFocusKey> = true
EnvironmentPropertyKey<IsPlatformFocusSystemEnabled> = false
EnvironmentPropertyKey<IsFocusedKey> = false
EnvironmentPropertyKey<FocusBridgeKey> = WeakBox<FocusBridge>(base: Optional(SwiftUI.FocusBridge))
EnvironmentPropertyKey<AllowedBehaviorsKey> = HostingControllerAllowedBehaviors(rawValue: 16)
EnvironmentPropertyKey<ActiveContextMenuKey> = ViewIdentity(seed: 0)
EnvironmentPropertyKey<Key> = Optional(SwiftUI.NavigationAuthority(host: Optional(<_TtGC7SwiftUI14_UIHostingViewGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7f97bcb0b670; frame = (0 0; 390 844); autoresize = W+H; gestureRecognizers = <NSArray: 0x600000d47cf0>; layer = <CALayer: 0x60000032e980>>)))
EnvironmentPropertyKey<PresentationModeKey> = Binding<PresentationMode>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.LocationBox<SwiftUI.FunctionalLocation<SwiftUI.PresentationMode>>, _value: SwiftUI.PresentationMode(isPresented: false))
EnvironmentPropertyKey<AccessibilityRequestFocusKey> = AccessibilityRequestFocusAction(onAccessibilityFocus: nil)
EnvironmentPropertyKey<AccentColorKey> = Optional(#007AFFFF)
EnvironmentPropertyKey<HostingViewOpenURLActionKey> = Optional(SwiftUI.OpenURLAction(handler: SwiftUI.OpenURLAction.Handler.system((Function)), isDefault: false))
EnvironmentPropertyKey<UndoManagerKey> = Optional(<NSUndoManager: 0x600002039270>)
EnvironmentPropertyKey<SceneSessionKey> = Optional(SwiftUI.WeakBox<__C.UISceneSession>(base: Optional(<UISceneSession: 0x600001641200; scene = <UIWindowScene: 0x7f97bcb0a040; sceneIdentifier: "sceneID:us.eidinger.DateExample-F8369E9A-67DA-48FC-BAB6-EC1A5D8D56CC">; role = UIWindowSceneSessionRoleApplication; sceneConfiguration = <UISceneConfiguration: 0x600001641300>; persistentIdentifier = F8369E9A-67DA-48FC-BAB6-EC1A5D8D56CC; userInfo = <(null): 0x0>)))
EnvironmentPropertyKey<SystemColorSchemeKey> = light
EnvironmentPropertyKey<ExplicitPreferredColorSchemeKey> = nil
EnvironmentPropertyKey<AccessibilityLargeContentViewerKey> = false
EnvironmentPropertyKey<EnabledTechnologiesKey> = AccessibilityTechnologies(technologySet: SwiftUI.(unknown context at $7fff5dc3e438).AccessibilityTechnologySet(rawValue: 0))
EnvironmentPropertyKey<AccessibilityButtonShapesKey> = false
EnvironmentPropertyKey<AccessibilityPrefersCrossFadeTransitionsKey> = false
EnvironmentPropertyKey<AccessibilityInvertColorsKey> = false
EnvironmentPropertyKey<AccessibilityReduceMotionKey> = false
EnvironmentPropertyKey<AccessibilityReduceTransparencyKey> = false
EnvironmentPropertyKey<AccessibilityDifferentiateWithoutColorKey> = false
EnvironmentPropertyKey<BackgroundInfoKey> = BackgroundInfo(layer: 0, groupCount: 0)
EnvironmentPropertyKey<VerticalUserInterfaceSizeClassKey> = Optional(SwiftUI.UserInterfaceSizeClass.regular)
EnvironmentPropertyKey<HorizontalUserInterfaceSizeClassKey> = Optional(SwiftUI.UserInterfaceSizeClass.compact)
EnvironmentPropertyKey<DisplayScaleKey> = 3.0
EnvironmentPropertyKey<ColorSchemeKey> = light
EnvironmentPropertyKey<ColorSchemeContrastKey> = standard
EnvironmentPropertyKey<DisplayGamutKey> = displayP3
EnvironmentPropertyKey<LegibilityWeightKey> = Optional(SwiftUI.LegibilityWeight.regular)
EnvironmentPropertyKey<DynamicTypeSizeKey> = large
EnvironmentPropertyKey<LayoutDirectionKey> = leftToRight
EnvironmentPropertyKey<ScenePhaseKey> = active
EnvironmentPropertyKey<TimeZoneKey> = America/Los_Angeles (fixed (equal to current))
EnvironmentPropertyKey<CalendarKey> = gregorian (current)
--- Environment Values - END ---

The inner works of the SwiftUI environment handling are hidden. This is ok for most situations. One noticeable drawback is that the printed result for Font is not helpful as its description does not contain the font name/style information.

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!