Skip to content

feat: acknowledge physical modal removal - #20

Draft
robmaceachern wants to merge 2 commits into
mainfrom
codex/modal-removal-acknowledgement
Draft

robmaceachern wants to merge 2 commits into
mainfrom
codex/modal-removal-acknowledgement

Conversation

@robmaceachern

@robmaceachern robmaceachern commented Sep 23, 2026

Copy link
Copy Markdown
Member

Summary

Add an optional onDidRemove callback to PresentableModal and WorkflowModals.Modal for work that must run after a modal is physically removed. Appearance callbacks are not that boundary: they can run before detachment, or not run again when an already-disappeared presenter removes content.

A presenter is the controller that owns a modal's container and decorations. The new callback reports removal of that presenter's instance, not the end of the content's lifetime everywhere.

How it works

  • Workflow modal initializers, mapping, type erasure and controller reuse carry the callback to the presenter. Reusing a controller refreshes the callback, including clearing it with nil.
  • Removal detaches the container and decorations, updates presentation/accessibility bookkeeping, and consumes the callback once. Delivery waits until the current update is coherent; callbacks may update the modal list without recursive delivery.
  • A modal accepted and withdrawn before the presenter loads is retired without loading its views or attaching it later. This also fixes eager loading on that path for callers that omit the callback.
  • Existing initializer calls remain valid, including the trailing presentation-completion closure. The callback defaults to nil; onDidPresent, dismissal-request APIs and forwarding policy retain their existing roles.

Boundaries and failures

Forwarding can remove a presentation from one host while another still presents the content, so a removal callback is not a global dismissal or cancellation signal. Requesting the same controller again during its exit does not cancel that exit: the latest callback still observes the old instance's removal, and a later update can create a new instance. Consumers should check current state or a request identifier before interpreting removal as completion of application work.

Hiding a window or cancelling an interactive dismissal does not remove the modal. Presenter destruction does not guarantee a callback. A request withdrawn before any presenter receives it has no physical instance to remove and produces no callback.

The UIKit lifetime-token API (modalPresenter.present / ModalLifetime.dismiss) is intentionally unchanged and does not expose removal completion. UIKit integrations constructing PresentableModal directly can opt in; both usage guides explain this boundary.

Validation

This remains a draft. CI is green on 476c81e6bc0d40b73bffbf78d1d108791f27947d. The separate automated-review request has not been acknowledged; no completed remote review is claimed.

  • Current-head UnitTests CI passed 84 tests, zero failures: 65 Modals and 19 WorkflowModals, including all 19 added tests. Runner: iPad Pro 11-inch (M5), iOS 26.2, Xcode 26.5. The same validation run passed the sample build and pinned SwiftFormat check.
  • The executed follow-up cases are test_removalCallback_loaded_uses_latest_callback_including_nil, test_removalCallback_requested_again_during_exit_still_removes_old_instance, and test_removalCallback_pending_removal_still_requested_is_retired.
  • Added tests in ModalPresentationViewControllerTests for visible/disappeared presenters, hidden windows, zero-duration transitions, latest/nil callbacks, replacement and reused controllers, interrupted entry, cancelled interaction, destruction, reentrant updates and existing trailing-closure behavior.
  • ModalHostContainerViewControllerTests.test_removal_callback_is_local_when_forwarded_modal_moves_to_inner_host covers the forwarding boundary. Two ModalContainerTests cover callback propagation through mapping/type erasure, reused controllers and duplicate keys.

Existing-caller compatibility: supported for the tested library scope by current-head regressions, including test_no_leaks_on_teardown, test_should_log_on_presentation_and_dismissal, test_accessibilityViewIsModal_restored_after_nested_dismiss and the trailing-closure compatibility test. The default-nil API still shares the intentional lazy-loading fix, covered by test_withdraw_before_loading_does_not_load_or_later_attach_modal. The unacknowledged remote-review request remains a separate review-evidence gap; no merge recommendation yet.

Visibility-independent removal: visible/disappeared presenter and hidden-window tests passed in public CI. These are UIKit-hosted and controlled transition tests, not OS-driven scene lifecycle qualification.

Checklist

  • Unit Tests
  • Documentation
  • Pull request title follows conventional commits

This response was drafted with AI assistance.

Add an optional callback after a presenter's modal container and
bookkeeping have been removed. Carry the latest callback through
workflow rendering and defer delivery until reconciliation completes
so callbacks can safely update the modal list.

Retire withdrawn, unloaded presentations without loading their views.
Document per-presenter forwarding semantics and the limits around
unaccepted requests and presenter destruction. Add focused regression
and compatibility tests without changing dismissal-request APIs.

---
*This response was drafted with AI assistance.*
Cover loaded callback freshness and physical removal when a controller
is requested again during an exit or pending-removal reconciliation.
Document that removal is not proof that content is no longer wanted.

Clarify the direct UIKit API boundary and preserve the rationale for
the existing trailing presentation callback's parameter position.

---
*This response was drafted with AI assistance.*
@robmaceachern

Copy link
Copy Markdown
Member Author

🤖 @codex review

Please review head 476c81e6bc0d40b73bffbf78d1d108791f27947d, focusing on physical-removal ordering, callback freshness and once-only delivery, unloaded presentations, reentrant updates, forwarding-local semantics, and existing-caller API compatibility.


This response was drafted with AI assistance.

This branch has not been deployed

No deployments
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.

1 participant