Repository navigation
Don't hold the delegate when UIKit refuses to show the Customer Center - #539
Conversation
UIKit refuses some presentations, such as from a view controller that isn't in a window, and only logs it. The manager stored the delegate and counted the presentation before asking, so the app's delegate stayed alive with nothing on screen. It now checks that UIKit took the controller first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maple review🟢 Confidence 9/10 · safe to merge Customer Center presentation now retains its delegate and updates presentation state only after UIKit accepts the controller. The regression test covers delegate release, skipped dismissal callbacks, and a successful subsequent presentation; the change is safe to merge. What was checked
Observability coverage: 1 of 1 changes observable
|
There was a problem hiding this comment.
ℹ️ No issues with the fix. One stale comment outside the diff is worth a look.
Reviewed changes
I reviewed the reordering in CustomerCenterManager.present and the new regression test.
- Detect refused presentations:
present(_:animated:)now runs first. The manager keepsretainedDelegateandpresentedControllerand bumpspresentCountonly when UIKit actually setcontroller.presentingViewController. Otherwise it logs an error and returns. - Regression test:
refusedPresentationReleasesDelegatepresents from a detached controller and checks that the delegate is released, nothing is counted, andonDismissdoesn't fire. It then confirms a later windowed present works. The test really pins the bug: on the old codeweakDelegatestays non-nil andpresentCountis1, so both assertions fail.
The delegate can't be freed partway through the call. While present runs, the delegate parameter holds it strongly, so any callback that fires during the synchronous part of the presentation still reaches it before retainedDelegate is assigned. A refused controller only reaches onDismiss through deliverDismissal(). That runs from viewDidDisappear/didMove(toParent: nil), and neither fires for a controller that was never shown. So skipping onDismiss holds up in practice, not just by the docs.
ℹ️ Nitpicks
- The doc comment on
wasPresentedModallyinCustomerCenterViewController.swift#L49-L51says a hostless test target "never populatespresentingViewController". This PR depends on the opposite. BothsingleInstance(animated) and the new test expectisPresentedright afterpresent, and that now requirespresentingViewControllerto be set synchronously. In the test target it'sviewDidAppearthat never runs, notpresentingViewControllerthat stays unset. The comment should say so, so it doesn't contradict the manager's new check.
claude-opus-5.5 | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maple review🟢 Confidence 10/10 · safe to merge The follow-up corrects the What was checked
|

What
CustomerCenterManager.presentstored the delegate, the controller and the presentation count before callingpresent(_:animated:). UIKit refuses some presentations, for example from a view controller that isn't in a window or is mid-transition, and only logs a warning when it does. In that case the manager kept the app's delegate alive with nothing on screen, until the next present replaced it.It now calls
presentfirst and only holds on to anything oncecontroller.presentingViewControlleris set. Otherwise it logs an error and returns.Greptile flagged this on #532 as "later presents are blocked". That part doesn't happen:
presentedControllerisweak, so a refused controller is freed straight away and the next present still works. The new test checks both.onDismissfor a refused presentation: it isn't called. Its docs say "Called after the Customer Center is dismissed", and a screen that never appeared wasn't dismissed.customerCenterDidDismiss()doesn't fire either, for the same reason.No CHANGELOG entry: the Customer Center hasn't been released yet (master is 4.17.0).
Tests
Added
refusedPresentationReleasesDelegatetoCustomerCenterManagerTests. It presents from a view controller with no window, then checks the delegate is released, nothing is counted as presented,onDismissdidn't run, and a following present from a real window still works. It failed on the old code (delegate kept, presentation counted) and passes now.CustomerCenterManagerTestsandCustomerCenterSheetOwnershipTestsall pass (41 tests).Checklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes. (Not needed: unreleased feature.)swiftlintin the main directory and fixed any issues.🤖 Generated with Claude Code
This PR appears safe to merge.
What we checked:
presentreturns before assigningretainedDelegatewhen the controller has no presenter. The adapter holds only weak references.Summary
CustomerCenterManager.presentnow asks UIKit to present before retaining the delegate or recording the presentation. If UIKit refuses, it logs an error and returns.Reviews (1) · Last reviewed commit: "Don't hold the delegate when UIKit refus..." · Reviewed by Greptile