Skip to content

Display a deprecation warning and fatal error when the top-level @Environment(\.viewEnvironment) is referenced #319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 28, 2025

Conversation

kyleve
Copy link
Contributor

@kyleve kyleve commented Jan 28, 2025

For UI-5972

Display a deprecation warning and fatal error when the top-level @Environment(\.viewEnvironment) is referenced, to enforce people reference a sub-property. This ensures proper SwiftUI view invalidation.

image

Checklist

  • Unit Tests
  • UI Tests
  • Snapshot Tests (iOS only)
  • I have made corresponding changes to the documentation

Sorry, something went wrong.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
…ironment(\.viewEnvironment) is referenced, to enforce people reference a sub-property. This ensures proper SwiftUI view invalidation.
@kyleve kyleve marked this pull request as ready for review January 28, 2025 19:58
@kyleve kyleve requested review from a team as code owners January 28, 2025 19:58
Instead, reference your relevant sub-property, eg `@Environment(\\.viewEnvironment.myProperty)`.
"""
)
@inlinable public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a case where we'd want to do this, but if we want to allow it, perhaps adding an SPI'd init(unsafe: ...) would allow that escape hatch.


@available(
*,
deprecated,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this needs to be a deprecation, not an unavailability, as if you mark the method as unavailable, the compiler simply falls back to the "regular" init.

Copy link
Member

@robmaceachern robmaceachern left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for doing this.

Copy link
Collaborator

@mjohnson12 mjohnson12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
Copy link
Contributor

@jamieQ jamieQ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks reasonable to me

"""
)
@inlinable public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
fatalError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is fatal'ing unconditionally the right approach? what exactly is the downside if one does this? if it's 'just' poor performance, should we be more lenient with the production code and make this an assertion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a fatal is correct / preferred, since you should never do this in production code. If we already had consumers of SwiftUI doing this, I'd probably go w/ an assert, but given we have the opportunity to "close the door" on this permanently, I'd say let's do that

message:
"""
Please do not create an `@Environment` property that references the top-level `viewEnvironment`: \
it will break SwiftUI's automatic invalidation when any part of the `ViewEnvironment` changes. \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for my own personal edification: can you elaborate on the risk & consequences a bit more? what is the behavioral difference in these 2 approaches?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically, the way that SwiftUI does state invalidation is to compare the properties on a view between the old view and the new view, and if they're different or otherwise not comparable, the view is redrawn and updated.

Because ViewEnvironment is a big opaque bag o' crap, its possible that there's non-comparable stuff in there, or there's stuff in there that changes every render cycle, meaning that it'd lead to view invalidation. So the fix for that is to scope your Environment property down to just the data you need.

I know there's a point free or objc.io article about this in more depth somewhere, however I am struggling to find it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! this makes sense – i had failed to realize that the root .viewEnvironment key is like putting a copy of another environment into the... SwiftUI.Environment, and clearly you don't want to depend on the entire environment itself, just the stuff your view 'cares about'.

@kyleve kyleve enabled auto-merge (squash) January 28, 2025 23:06
@kyleve kyleve merged commit 0811a5d into main Jan 28, 2025
7 checks passed
@kyleve kyleve deleted the kve/deprecat-top-level-view-environment branch January 28, 2025 23:11
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.

None yet

5 participants