Skip to content

[Experimental] Opt-in @Environment resolution to prevent warnings during inspection - #416

Draft
garry-jeromson wants to merge 1 commit into
nalexn:0.10.4from
garry-jeromson:feature/environment-resolution
Draft

[Experimental] Opt-in @Environment resolution to prevent warnings during inspection#416
garry-jeromson wants to merge 1 commit into
nalexn:0.10.4from
garry-jeromson:feature/environment-resolution

Conversation

@garry-jeromson

@garry-jeromson garry-jeromson commented Mar 1, 2026

Copy link
Copy Markdown

Problem

When ViewInspector evaluates a custom view's body, any @Environment properties still in their .keyPath state trigger:

Accessing Environment<T>'s value outside of being installed on a View. This will always read the default value and will not update.

In large test suites this can produce thousands of warnings (e.g. ~4,000 in ours), making it difficult to spot real issues in test output.

Related issues: #131, #167, #338

Solution

This PR adds opt-in @Environment resolution that transitions fields from .keyPath to .value state before body evaluation, eliminating the warnings entirely.

Enabling

// Per-test
override func setUp() {
    ViewInspectorConfig.resolveEnvironmentValues = true
}

// Or via environment variable (e.g. in your test scheme/CI)
VIEWINSPECTOR_RESOLVE_ENVIRONMENT=1

Custom environment values

Test code using .environment() modifiers is automatically honored during resolution — both standard SwiftUI keys and custom EnvironmentKey types:

// Standard key override
let sut = MyView().environment(\.colorScheme, .dark)
let text = try sut.inspect().find(ViewType.Text.self).string()
XCTAssertEqual(text, "dark") // ✅ Uses the override, not the default

// Custom EnvironmentKey
let sut = MyView().environment(\.myCustomKey, "custom_value")
let text = try sut.inspect().find(ViewType.Text.self).string()
XCTAssertEqual(text, "custom_value") // ✅ Custom keys work too

Technical approach

  • EnvironmentResolvable protocol — conformance on Environment<Value> enables type-erased generic resolution via _resolve(using:)
  • AnyWritableEnvironmentKeyPath protocol — conformance on WritableKeyPath<EnvironmentValues, T> enables type-erased application of .environment() modifier overrides
  • Raw byte manipulation of @frozen @Environment<Value> enum layout (.keyPath.value state transition)
  • Keypath-pointer-only matching — when sizeof(Value) > sizeof(KeyPath), Mirror-extracted copies have divergent spare payload bytes; matching only the keypath pointer (which is unique per field) avoids false negatives
  • ARC safety — action/closure-based environment values are skipped; buildValueStateBytes uses raw memory allocation to avoid corrupting reference-counted keypath fields
  • Thread safety — environment values flow through the call chain as parameters, no mutable static state

⚠️ Experimental — caveats

This is an experimental approach that relies on @Environment's @frozen ABI-stable layout. It works reliably in our test suite but should be considered carefully before broader adoption:

  1. @frozen dependency — the implementation assumes @Environment<Value> uses a two-case enum (keyPath/value) with a specific byte layout. This is ABI-stable but not a public API contract.

  2. Value types only — resolution is skipped for @Environment values that contain reference types (closures, classes). Action-type environments like \.dismiss and \.openURL are safely ignored.

  3. Resolved values are defaults unless overridden — without .environment() modifiers, values come from EnvironmentValues() (SwiftUI defaults). Custom keys use their EnvironmentKey.defaultValue.

Files changed

File Description
EnvironmentInjection.swift Core resolution logic, ViewInspectorConfig, EnvironmentResolvable, AnyWritableEnvironmentKeyPath
ContentExtraction.swift Hook points: resolve before copy.body / copy.body(), thread environmentModifiers parameter
CustomView.swift Pass medium.environmentModifiers through to extractContent
EnvironmentInjectionTests.swift 16 test cases covering protocol, resolution, integration, custom keys, and regression

Test coverage

  • ✅ Bool, enum, action environment types
  • ✅ Resolution enabled/disabled (opt-in flag)
  • ✅ Single and multiple @Environment fields
  • ✅ Mixed @Environment + @EnvironmentObject
  • ✅ ViewModifier with @Environment
  • ✅ Standard key override via .environment() modifier
  • ✅ Custom EnvironmentKey override via .environment() modifier
  • ✅ Default values when no override present
  • ✅ Existing @EnvironmentObject injection unaffected
  • ✅ All 1,480 existing ViewInspector tests pass

@garry-jeromson
garry-jeromson force-pushed the feature/environment-resolution branch 2 times, most recently from f5c27da to bfd3438 Compare March 2, 2026 00:04
@garry-jeromson
garry-jeromson marked this pull request as draft March 2, 2026 00:15
…ing inspection

When ViewInspector evaluates a custom view's body, @Environment properties in
.keyPath state trigger "Accessing Environment<T>'s value outside of being
installed on a View" warnings. This adds opt-in resolution that transitions
@Environment fields from .keyPath to .value state before body evaluation.

Key features:
- Opt-in via ViewInspectorConfig.resolveEnvironmentValues or env var
- Supports standard SwiftUI keys and custom EnvironmentKey types
- Honors .environment() modifier overrides from test code
- Thread-safe: values flow through the call chain, no mutable statics
- Skips action/closure-based environment values (ARC safety)

Technical approach:
- EnvironmentResolvable protocol for type-erased generic resolution
- AnyWritableEnvironmentKeyPath protocol for type-erased modifier application
- Raw byte manipulation of @Frozen @Environment<Value> enum layout
- Keypath-pointer-only matching to handle Mirror spare byte divergence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@garry-jeromson
garry-jeromson force-pushed the feature/environment-resolution branch from bfd3438 to 2583e6d Compare March 2, 2026 00:36
@nalexn

nalexn commented Mar 2, 2026

Copy link
Copy Markdown
Owner

This is very interesting, thanks for the PR! I hope I'll get some free time soon to explore this more and merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants