Skip to content

refactor(iOS, FormSheet v5): Move AppearanceCoordinator/Applicator to ContentController#4074

Merged
t0maboro merged 18 commits into
mainfrom
@t0maboro/move-config-to-controller
May 22, 2026
Merged

refactor(iOS, FormSheet v5): Move AppearanceCoordinator/Applicator to ContentController#4074
t0maboro merged 18 commits into
mainfrom
@t0maboro/move-config-to-controller

Conversation

@t0maboro

@t0maboro t0maboro commented May 21, 2026

Copy link
Copy Markdown
Contributor

Description

Previously, there was a tight coupling where RNSFormSheetContentController and RNSFormSheetHostComponentView had a direct dependency. By introducing dedicated provider protocols, the UIKit is now completely agnostic of React.

Closes: https://github.com/software-mansion/react-native-screens-labs/issues/1454

Changes

  • defining contracts that Host now implements to supply data.
  • removed all RNSFormSheetHostComponentView dependencies from RNSFormSheetContentController and RNSFormSheetAppearanceApplicator
  • moved invalidation flags logic from the Host to the Controller

Before & after - visual documentation

N/A - refactor

Test plan

No regression in existing examples

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the iOS FormSheet (v5) implementation to align with the Split host pattern: the host component now only records invalidation flags on prop/window changes, and the content controller owns the appearance coordinator/applicator and performs the batched updates after React mounting transactions complete.

Changes:

  • Moved RNSFormSheetAppearanceCoordinator/RNSFormSheetAppearanceApplicator ownership and update execution into RNSFormSheetContentController.
  • Added invalidation flags on RNSFormSheetHostComponentView and forwarded them to the controller in finalizeUpdates / didMoveToWindow.
  • Wired mounting-transaction callbacks from the host to the controller so the controller flushes pending appearance/presentation work in mountingTransactionDidMount.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
ios/gamma/modals/form-sheet/RNSFormSheetHostComponentView.mm Replaces direct appearance/presentation updates with invalidation flags + controller signals, and forwards mounting-transaction callbacks.
ios/gamma/modals/form-sheet/RNSFormSheetHostComponentView.h Exposes isOpen to allow the controller to derive presentation state from host props.
ios/gamma/modals/form-sheet/RNSFormSheetContentController.mm Takes over appearance coordinator/applicator and executes pending appearance/presentation updates after mounting transactions.
ios/gamma/modals/form-sheet/RNSFormSheetContentController.h Adds controller “signal” APIs, mounting-transaction hooks, and a weak back-reference to the host view.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@kkafar kkafar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have few remarks. Please answer them before proceeding.

Comment on lines +36 to +37
- (void)reactMountingTransactionWillMount;
- (void)reactMountingTransactionDidMount;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It was my mistake to name these like this.

The content controller should ideally not be aware of the react inner workings.
We also likely don't need will callback.

Single flushPendingUpdates should be enough. Just as it is done in tabs.

Comment on lines +120 to +123
if (_needsInitialDetentReset) {
_needsInitialDetentReset = NO;
[_appearanceApplicator resetInitialDetent];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do my first reading & don't understand the code fully yet, but detent update in appearance method doesn't fit for me. Aren't detents rather a behaviour?

Thinking now. Maybe call it configuration or something and then have both detents and appearance here.

@t0maboro t0maboro May 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

you're right, there are a few things I don't like here:

  • appearance & behavior separation
  • that AppearanceApplicator is not stateless

my current plan is to:

  • separate appearance and behavior on Providers level for this PR
  • refactor AppearanceApplicator to extract Behavior configuration (next PR)
  • make AppearanceApplcator stateless (next PR)

let me know if you're okay with that


#pragma mark - Appearance

- (void)updateAppearanceIfNeededForHost:(nonnull RNSFormSheetHostComponentView *)host

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

...ForHost suggests that you apply the appearance configuration on the host object, which is not the case. I'd name it ...withConfigProvider or somethig like this.

This method ideally, shouldn't take RNSFormSheetHostComponentView, but rather an interface of a delegate / provider. This is not a priority though.

Comment on lines +27 to +30
// Invalidation flags
BOOL _needsPresentationUpdate;
BOOL _needsAppearanceUpdate;
BOOL _needsInitialDetentReset;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should these live on the host?

All the invalidation flags should leave on the content controller. Take a look at how Tabs are implemented. Host & Screens report the need (directly or not) of the update to the controller & it holds it, and clears the flag when it runs the update logic.

We can talk about it some more if that's not fully clear.

}];
if (_needsPresentationUpdate) {
_needsPresentationUpdate = NO;
[_controller setNeedsPresentationUpdate];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of holding these additional flags on the host and passing this information to the controller here we can directly signalise to the controller when we e.g. detect prop change.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread ios/gamma/modals/form-sheet/RNSFormSheetHostComponentView.mm Outdated
Comment thread ios/gamma/modals/form-sheet/RNSFormSheetContentController.h
Comment thread ios/gamma/modals/form-sheet/RNSFormSheetAppearanceApplicator.mm

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread ios/gamma/modals/form-sheet/RNSFormSheetHostComponentView.mm Outdated
Comment thread ios/gamma/modals/form-sheet/RNSFormSheetContentController.mm Outdated
controller:(RNSFormSheetContentController *)controller
coordinator:(RNSFormSheetAppearanceCoordinator *)coordinator;
- (void)updateAppearanceIfNeededForAppearanceProvider:(id<RNSFormSheetAppearanceProvider>)appearanceProvider
behaviorProvider:(id<RNSFormSheetBehaviorProvider>)behaviorProvider

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

note: behavior will have a dedicated path, but I'm extracting it to a separate PR in which I'm going to introduce a dedicated class for that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

associated followup PR: #4084

t0maboro and others added 2 commits May 22, 2026 11:42
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@kkafar kkafar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good. I still have few remarks / questions. Let's answer them.

- (void)updateAppearanceIfNeededForHost:(RNSFormSheetHostComponentView *)host
controller:(RNSFormSheetContentController *)controller
coordinator:(RNSFormSheetAppearanceCoordinator *)coordinator;
- (void)updateAppearanceIfNeededForAppearanceProvider:(id<RNSFormSheetAppearanceProvider>)appearanceProvider

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have one remark regarding naming.

Suggested change
- (void)updateAppearanceIfNeededForAppearanceProvider:(id<RNSFormSheetAppearanceProvider>)appearanceProvider
- (void)updateAppearanceIfNeededWithAppearanceProvider:(id<RNSFormSheetAppearanceProvider>)appearanceProvider

The For part does sound inappropriate and misleading in this context.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines +99 to +102
- (nullable UIWindow *)hostWindow
{
return self.window;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand what hostWindow is for. The window is shared for both host & content hierarchies. Why do we need additional method here? Do you need window earlier than onWillMoveToWindow on RNSFormSheetContentController.view?

@t0maboro t0maboro May 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When I navigate to the screen that has FormSheet with initially isOpen=true set, what I'm observing is that on the 1st mounting transaction, flushPendingUpdates updates the presentation and calls updatePresentationState before willMoveToWindow is called on ContentView

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay, but why do you even need a window for before you receive willMoveToWindow?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updatePresentationState triggers presentFromWindow and in that method I'm searching for a VC that will present this ContentController

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay. I don't like the approach that RNSFormSheetContentController is responsible for its own presentation, however it's not crucial atm, it's more important to progress.

Let's create a ticket to clean this up.

Host should be responsible for presentation of the content controller and the logic should leave there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines +106 to +119
- (BOOL)prefersGrabberVisible
{
return _prefersGrabberVisible;
}

- (CGFloat)preferredCornerRadius
{
return _preferredCornerRadius;
}

- (NSInteger)largestUndimmedDetentIndex
{
return _largestUndimmedDetentIndex;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are these necessary? You have declared properites on RNSFormSheetHostComponentView.h, aren't they synthesized by the compiler?

@t0maboro t0maboro May 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

dc3f9ac
note: having these providers, I think that we shouldn't expose properties from HostComponent at all, but rather operate on proper providers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry, I think I didn't communicate clearly.

These properties SHOULD exist on RNSFormSheetHostComponentView interface. We always expose the props.

My remark is that you don't have to write their getters in .mm file. IIRC the getters should be synthesized by the compiler.

@t0maboro t0maboro May 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

correct, these getters will be synthesized, okay will expose props

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's fix the code then. Restore the properties on the interface and remove unnecessary implementations.

@t0maboro t0maboro requested a review from kkafar May 22, 2026 11:15
@t0maboro t0maboro merged commit d0b645e into main May 22, 2026
5 of 6 checks passed
@t0maboro t0maboro deleted the @t0maboro/move-config-to-controller branch May 22, 2026 12:36
t0maboro added a commit that referenced this pull request May 27, 2026
## Description

A follow-up for #4074 . It addresses the overloaded "Appearance" concept
by logically separating visual updates from behavioral updates.

Previously, both visual props (e.g. grabber visibility) and structural
props (e.g. detents) were grouped together under a single "Appearance"
applicator. This PR introduces a more granular UpdateCoordinator and
separates the logic.

## Changes

- Renamed `RNSFormSheetAppearanceCoordinator` to
`RNSFormSheetUpdateCoordinator`.
- Replaced old flags with `RNSFormSheetUpdateFlags`, having distinct
Appearance, Behavior and Presentation flags.
- Replaced `RNSFormSheetAppearanceApplicator` with
`RNSFormSheetConfigurationApplicator` that processes both Appearance and
Behavior providers and applies them inside a single block.
- Refactored RNSFormSheetContentController to separate
`updateConfigurationIfNeeded` from `updatePresentationIfNeeded`.
`flushPendingUpdates` now configures the sheet first, then presents it.

## Before & after - visual documentation

N/A - refactor

## Test plan

Tested with existing SFTs for FormSheet.

## Checklist

- [ ] Included code example that can be used to test this change.
- [ ] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [ ] For API changes, updated relevant public types.
- [ ] Ensured that CI passes
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