Skip to content

fix: Resolve iOS retain cycle in EditorViewController async flow#333

Merged
dcalhoun merged 1 commit intotrunkfrom
fix/prevent-ios-retain-cycle
Feb 24, 2026
Merged

fix: Resolve iOS retain cycle in EditorViewController async flow#333
dcalhoun merged 1 commit intotrunkfrom
fix/prevent-ios-retain-cycle

Conversation

@dcalhoun
Copy link
Copy Markdown
Member

@dcalhoun dcalhoun commented Feb 24, 2026

What?

Fixes a retain cycle that prevents EditorViewController from being deallocated when using the async dependency-loading flow.

Why?

Fix CMM-1282.

When opening and closing the editor multiple times without pre-loaded dependencies, each EditorViewController instance leaks — deinit is never called and Safari's Web Inspector shows lingering web views.

The root cause is EditorService.prepare() storing the progress callback in a property that is never cleared. Since EditorViewController owns the EditorService and the callback captures the view controller, this creates a retain cycle:

EditorViewController → editorService → progressCallback → EditorViewController

This only affects the async flow (no pre-loaded dependencies) because:

  • Offline/default editor: prepare() returns early before storing the callback
  • Pre-loaded dependencies: The async flow is never entered

In WordPress-iOS, the issue is reproducible by using the pull-to-refresh gesture on My Site to invalidate the editor dependencies cache, then opening the editor.

How?

  1. EditorService.swift: Added defer { self.progressCallback = nil; self.progress = nil } in prepare() to clear the stored callback after completion, breaking the cycle from the service side.

  2. EditorViewController.swift: Added [weak self] captures in the Task closure and progress callback as defense-in-depth, ensuring the cycle cannot form even if the service retains the callback.

Either fix alone resolves the issue; both together provide robustness.

Testing Instructions

  1. Open the iOS demo app
  2. Configure a site editor (do not tap "Prepare Editor" to pre-load dependencies)
  3. Open the editor — it will load dependencies asynchronously
  4. Close the editor
  5. Repeat steps 3–4 several times
  6. Verify in Safari Web Inspector that old web views no longer linger
  7. Optionally add a deinit to EditorViewController and confirm it fires on each dismissal

EditorService.prepare() stored the progress callback permanently,
creating a retain cycle when EditorViewController owned the service
and the callback captured the view controller. Clear the callback
on completion and use weak captures in the Task and closure.
@dcalhoun dcalhoun added the [Type] Bug An existing feature does not function as intended label Feb 24, 2026
@dcalhoun dcalhoun marked this pull request as ready for review February 24, 2026 21:03
@dcalhoun dcalhoun requested a review from jkmassel February 24, 2026 21:03
@dcalhoun dcalhoun merged commit b8b4852 into trunk Feb 24, 2026
13 checks passed
@dcalhoun dcalhoun deleted the fix/prevent-ios-retain-cycle branch February 24, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants