Skip to content
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

[feat]: add imperative environment customization support #231

Merged
merged 9 commits into from
Jul 26, 2023

Conversation

n8chur
Copy link
Collaborator

@n8chur n8chur commented Jul 13, 2023

Overview

Adds support for declaring environment customizations to a ViewEnvironmentPropagatingNode without subclassing, custom wiring, or VC containment.

Kudos to @watt for the idea!

Checklist

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

@n8chur n8chur force-pushed the westin/imperative-environment-customization branch from 8810208 to c67b999 Compare July 13, 2023 22:56
@n8chur n8chur requested review from watt and a team July 24, 2023 19:52
@n8chur n8chur changed the title [WIP DNR] [feat]: add imperative environment customization support [WIP] [feat]: add imperative environment customization support Jul 24, 2023
@n8chur n8chur changed the title [WIP] [feat]: add imperative environment customization support [feat]: add imperative environment customization support Jul 24, 2023
@n8chur n8chur marked this pull request as ready for review July 24, 2023 22:25
@n8chur n8chur requested a review from a team as a code owner July 24, 2023 22:25
@@ -351,17 +380,21 @@ public final class ViewEnvironmentUpdateObservationLifetime {
/// This is called in `deinit`.
Copy link
Contributor

Choose a reason for hiding this comment

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

is this still true?

Copy link
Collaborator Author

@n8chur n8chur Jul 25, 2023

Choose a reason for hiding this comment

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

Ah, yeah I should add some nuance to this comment. The closure provided for onRemove is only called in deinit if it was not called manually.

  • (7242f1f) Improve doc comment regarding remove() being called in deinit

@@ -351,17 +380,21 @@ public final class ViewEnvironmentUpdateObservationLifetime {
/// This is called in `deinit`.
///
public func remove() {
guard let onRemove else {
preconditionFailure("Environment customization was already removed")
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems like an assert warning about unexpected behavior may be more appropriate. this is idempotent, right (other than the precondition)? will something break horribly if it's called more than once?

Copy link
Collaborator Author

@n8chur n8chur Jul 25, 2023

Choose a reason for hiding this comment

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

Nothing would break if this was called more than once with the current implementation.

I'm fine with moving this to an assert with early return, but I'll check with @watt who suggested this change!

  • (5b7a48d) Use assert instead of preconditionFailure?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Asserting is fine with me. I think the idea here is that we consider it a programmer error to try to remove a customization more than once, even if it is effectively idempotent.

Copy link
Collaborator Author

@n8chur n8chur Jul 25, 2023

Choose a reason for hiding this comment

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

Oh whoops, I added this to the wrong lifetime 🤦

  • (39d493d) Apply removal assertion changes to customization lifetime

@n8chur n8chur requested a review from jamieQ July 25, 2023 18:53
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.

this all looks reasonable to me!

/// until `remove()` is called on it.
///
@_spi(ViewEnvironmentWiring)
public func addEnvironmentCustomization(
Copy link
Contributor

Choose a reason for hiding this comment

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

just to better my own understanding – should this trigger the 'setNeedsEnvironmentUpdate' (or whatever its name is) logic, or does that responsibility lie at a different level?

Copy link
Collaborator Author

@n8chur n8chur Jul 26, 2023

Choose a reason for hiding this comment

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

Yeah—I'm fairly torn on whether or not this should automatically call setNeedsEnvironmentUpdate() on your behalf. You'll almost always want to call it after adding a customization, but I figured there may be cases where you'd prefer to defer that call or manually manage when the invalidation occurs (e.g. for performance reasons). setNeedsEnvironmentUpdate() does trigger environmentDidChange() calls which are not deferred to the next layout cycle, and we use this to perform updates in some cases within WorkflowUI and Market. For example, WorkflowHostingController will re-render its content whenever environmentDidChange() is called.

The current shape also makes it a bit easier to ensure events happen at more appropriate times. For example, if you were to be adding a customization to a new child VC in a container VC, you might have something like this:

addChild(childVC)
customizationLifetime = childVC
    .addEnvironmentCustomization(makeCustomization(for: column))
if let view = viewIfLoaded {
    view.add(childVC.view)
}
childVC.didMove(toParent: self)
childVC.setNeedsEnvironmentUpdate()

This order of operations means:

  • The child VC is added as a child and the customization is added before the child VC's view is (conditionally) loaded. This ensures any environment access during viewDidLoad would have the appropriate environment wiring path and customizations.
  • setNeedsEnvironmentUpdate() is called after the parent/child relationship is fully established (didMove(toParent:) has been called). I'm not aware of any real issues that could come from calling this before didMove(toParent:), but it seems like the most appropriate time in the VC lifecycle IMO.

I'm very open to automatically calling setNeedsEnvironmentUpdate() automatically when a customization is added if we prefer the safety here!

@@ -119,6 +119,10 @@ extension ViewEnvironmentPropagating {
public var environment: ViewEnvironment {
Copy link
Contributor

Choose a reason for hiding this comment

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

more of a general question about the changeset – is this alternate API mainly for convenience? curious what the motivating examples are (if they exist).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's mainly for convenience. The motivating example is support for what we call "pane-based size class", where a split container VC is able to override the horizontal size class for a particular pane based on the available horizontal width of that pane.

Without this change we'd either need to insert a node between the split container VC and it's children, or introduce a wrapper VC around those children to perform the environment mutation.

@n8chur n8chur merged commit bec8cc8 into main Jul 26, 2023
12 checks passed
@n8chur n8chur deleted the westin/imperative-environment-customization branch July 26, 2023 18:45
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.

3 participants