Skip to content

[iOS] Fix Font autoscaling does not work in realtime#34445

Open
HarishwaranVijayakumar wants to merge 6 commits intodotnet:mainfrom
HarishwaranVijayakumar:fix-34307
Open

[iOS] Fix Font autoscaling does not work in realtime#34445
HarishwaranVijayakumar wants to merge 6 commits intodotnet:mainfrom
HarishwaranVijayakumar:fix-34307

Conversation

@HarishwaranVijayakumar
Copy link
Copy Markdown
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Issue Details

  • When the FontSize is set on a control, font scaling is correctly applied at app startup based on the system's font scaling setting. However, if the user changes the system font size while the app is running, the font does not dynamically update — the app must be killed and restarted for the new font size to take effect.

Root Cause of the issue

  • FontManager.iOS.cs caches scaled UIFont instances in a ConcurrentDictionary<Font, UIFont>. The cache key is the Font struct (family, size, weight, etc.).Once cached, the same UIFont is returned on every subsequent request — even after the user changes the Dynamic Type size — because the Font struct key hasn't changed.

Description of Change

Dynamic Type font scaling improvements:

  • src/Core/src/Fonts/FontManager.iOS.cs: Added an observer for ContentSizeCategoryChanged notifications to clear the font cache in _fonts, ensuring new fonts are generated with the correct scaling when the user's preferred content size category changes.
  • src/Core/src/Platform/iOS/PageViewController.cs: In TraitCollectionDidChange, added logic to detect changes in the preferred content size category and call InvalidateFontsOnContentSizeChanged to update fonts across all text elements in the current view.
  • src/Core/src/Platform/iOS/PageViewController.cs: Introduced the InvalidateFontsOnContentSizeChanged method, which recursively updates font values and invalidates measurements for all text elements with auto-scaling enabled, ensuring immediate UI updates.

Issues Fixed

Fixes #34307

Tested the behaviour in the following platforms

  • - Windows
  • - Android
  • - iOS
  • - Mac
Before After
Before.mov
After.mov

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 12, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34445

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34445"

@dotnet-policy-service dotnet-policy-service bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Mar 12, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review March 13, 2026 10:14
Copilot AI review requested due to automatic review settings March 13, 2026 10:14
@sheiksyedm
Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests,maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes iOS (and MacCatalyst) Dynamic Type changes not being reflected at runtime for explicitly-sized fonts by ensuring cached scaled fonts are invalidated and text elements are refreshed when the preferred content size category changes.

Changes:

  • Clear the iOS FontManager font cache when ContentSizeCategoryChanged is raised.
  • Detect preferred content size category changes in PageViewController.TraitCollectionDidChange and refresh fonts/measurements across the current visual tree for autoscaled text.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Core/src/Platform/iOS/PageViewController.cs Refreshes autoscaled text fonts + invalidates measure when Dynamic Type category changes.
src/Core/src/Fonts/FontManager.iOS.cs Clears the cached UIFont instances on Dynamic Type category change so new scaled fonts are generated.

@kubaflo kubaflo added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 13, 2026
@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Mar 14, 2026

🤖 AI Summary

📊 Expand Full Review501eb46 · Refactor
🔍 Pre-Flight — Context & Validation

Issue: #34307 - Font autoscaling does not work in realtime
PR: #34445 - [iOS] Fix Font autoscaling does not work in realtime
Platforms Affected: iOS (and MacCatalyst)
Files Changed: 2 implementation, 0 test

Key Findings

  • Root Cause: FontManager.iOS.cs caches scaled UIFont instances in a ConcurrentDictionary<Font, UIFont>. When the user changes Dynamic Type size at runtime, the Font struct key doesn't change so the same stale UIFont is returned from cache. The fonts never update until app restart.
  • Fix Area 1: FontManager.iOS.cs — Adds observer for UIApplication.Notifications.ObserveContentSizeCategoryChanged to clear _fonts cache when Dynamic Type changes.
  • Fix Area 2: PageViewController.cs — In TraitCollectionDidChange, detects changes to PreferredContentSizeCategory and calls InvalidateFontsOnContentSizeChanged to recursively refresh all autoscaled text elements.
  • Fix Area 3: PageViewController.cs — New static method InvalidateFontsOnContentSizeChanged recursively walks the visual tree via IVisualTreeElement.GetVisualChildren() and calls UpdateValue(nameof(ITextStyle.Font)) + InvalidateMeasure() for every ITextStyle element with AutoScalingEnabled: true.
  • Copilot Review Comment: The NSObject token returned by ObserveContentSizeCategoryChanged is not retained/disposed, which can cause FontManager instance leaks in tests. Should store and dispose the observer token in FontManager's dispose path.
  • No tests exist in this PR — No files in TestCases.HostApp or TestCases.Shared.Tests. Gate phase will be ⚠️ SKIPPED.
  • Issue confirmed reproducible on iOS by MAUI team member; not reproduced on Android.
  • PR author tested on Windows, Android, and iOS (not Mac).

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34445 Clear FontManager cache on ContentSizeCategoryChanged + recursively refresh autoscaled fonts in PageViewController.TraitCollectionDidChange ⏳ PENDING (Gate) FontManager.iOS.cs, PageViewController.cs Original PR

🚦 Gate — Test Verification

Gate Result: ⚠️ SKIPPED

Platform: iOS
Mode: N/A — No tests found in PR

Reason: PR #34445 contains no test files. Files changed:

  • src/Core/src/Fonts/FontManager.iOS.cs (implementation only)
  • src/Core/src/Platform/iOS/PageViewController.cs (implementation only)

No files exist in TestCases.HostApp or TestCases.Shared.Tests.

  • Tests FAIL without fix: ⚠️ N/A (no tests to run)
  • Tests PASS with fix: ⚠️ N/A (no tests to run)

Recommendation: Use write-tests-agent to create UI tests for issue #34307 (Font autoscaling realtime update on iOS).


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-sonnet-4.6) Bypass cache for auto-scaled fonts (never cache them; check AutoScalingEnabled in GetFont and call factory directly) 🚫 Blocked (no test) FontManager.iOS.cs, PageViewController.cs Proactive approach; simpler than PR
2 try-fix (claude-opus-4.6) Dual-cache (_fonts + _scaledFonts), add ClearScaledFontCache(), PageViewController calls via DI 🚫 Blocked (no test) FontManager.iOS.cs, PageViewController.cs No observer leak; selective invalidation
PR PR #34445 ObserveContentSizeCategoryChanged clears whole cache + recursive visual-tree refresh in PageViewController ⚠️ Gate skipped (no tests) FontManager.iOS.cs, PageViewController.cs Has observer token leak issue

Attempt 1 (claude-sonnet-4.6)

Approach: Bypass font cache for auto-scaled fonts proactively (no observer in FontManager)
Key Insight: Auto-scaled fonts call UIFontMetrics.DefaultMetrics.GetScaledFont(), whose output depends on UIContentSizeCategory. Caching them is semantically incorrect — they should be recreated fresh. The PR clears the cache reactively; this fix removes caching for auto-scaled fonts entirely.
Different from PR: PR uses ObserveContentSizeCategoryChanged to clear ALL cached fonts. This approach never caches auto-scaled fonts.
Result: 🚫 Blocked — No UI test available to empirically verify.

Attempt 2 (claude-opus-4.6)

Approach: Dual-cache architecture — split _fonts (non-scaled) and _scaledFonts (auto-scaled); add ClearScaledFontCache() on FontManager; PageViewController resolves FontManager via DI and calls it on trait change. No observer subscription.
Different from PR: No observer (eliminates leak), no full cache clear, selective invalidation of only auto-scaled fonts. Also different from Attempt 1 (still caches auto-scaled fonts between changes).
Result: 🚫 Blocked — No UI test available to empirically verify.

Cross-Pollination

Model Round New Ideas? Details
claude-sonnet-4.6 2 Yes Handler-level subscription: each handler (LabelHandler, EntryHandler etc.) subscribes to UIContentSizeCategoryDidChangeNotification directly and re-maps its own font property. No tree walk needed.
claude-opus-4.6 2 Yes Include UIApplication.SharedApplication.PreferredContentSizeCategory in font cache KEY — lookups naturally miss after Dynamic Type change; no observers, no clearing, no tree walks.

Note: Both cross-pollination ideas were NOT separately implemented as try-fix attempts since all attempts result in Blocked (no test exists to verify). They are documented for informational purposes.

Exhausted: Yes (2 models × Round 1, cross-pollination complete)
Selected Fix: PR's Fix — it is the only empirically-tested candidate (the PR author tested on iOS). However, it has a known issue: the observer token from ObserveContentSizeCategoryChanged is not retained/disposed, creating a potential memory leak. Recommend REQUEST CHANGES to fix the observer token lifecycle.


📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue #34307, 2 implementation files, 0 test files
Gate ⚠️ SKIPPED No tests in PR — cannot verify bug is caught
Try-Fix ✅ COMPLETE 2 attempts, 0 passing (all Blocked — no test available)
Report ✅ COMPLETE

Summary

PR #34445 fixes a real iOS bug where FontManager caches UIFont instances keyed by Font struct. When the user changes Dynamic Type at runtime, the cache key doesn't change, so the stale scaled font is returned until app restart. The PR correctly identifies and addresses this by:

  1. Registering a ContentSizeCategoryChanged observer in FontManager to clear the cache
  2. Detecting PreferredContentSizeCategory changes in PageViewController.TraitCollectionDidChange
  3. Recursively invalidating auto-scaled text elements via InvalidateFontsOnContentSizeChanged

The fix approach is sound and the PR author has manually tested on iOS. However, there is one confirmed code quality issue that needs to be addressed before merge.

Root Cause

FontManager.iOS.cs caches scaled UIFont instances in a ConcurrentDictionary<Font, UIFont>. The Font struct key does not include the current UIContentSizeCategory. When the user changes Dynamic Type, the Font key is unchanged, so _fonts.GetOrAdd(font, factory) returns the stale cached font instead of creating a new one with the updated scaling.

Fix Quality

What's Good:

  • Correct two-pronged approach: clear stale cache + refresh live UI elements
  • Uses ITextStyle.Font.AutoScalingEnabled guard to avoid unnecessary updates
  • Recursive tree walk via IVisualTreeElement.GetVisualChildren() is the right approach for pushing mapper updates
  • Correctly handles previousTraitCollection is not null guard

Issue That Requires Change:

Observer memory leak in FontManager.iOS.csUIApplication.Notifications.ObserveContentSizeCategoryChanged(...) returns a NSObject observer token. The PR discards this token (no variable assigned), which means:

  • The observer is never unregistered
  • The closure captures _fonts (hence FontManager instance), preventing GC
  • In test environments that create FontManager directly, this leaks per-instance

Fix: Store the returned NSObject in an instance field, implement IDisposable, and call _contentSizeCategoryObserver?.Dispose() in Dispose().

Example:

// In class fields:
NSObject? _contentSizeCategoryObserver;

// In constructor:
_contentSizeCategoryObserver = UIApplication.Notifications.ObserveContentSizeCategoryChanged(
    (sender, args) => _fonts.Clear());

// In Dispose():
_contentSizeCategoryObserver?.Dispose();
_contentSizeCategoryObserver = null;

Minor Concern (non-blocking):

  • InvalidateFontsOnContentSizeChanged is called regardless of whether CurrentView itself has auto-scaled fonts at the top level. This is fine for correctness but slightly wasteful if the page has no auto-scaled fonts at all (the tree walk still occurs).

Alternative Approaches Found in Try-Fix

Two alternative approaches were explored (both Blocked due to no tests):

  1. Bypass caching for auto-scaled fonts (Attempt 1): Never cache auto-scaled fonts at all — check font.AutoScalingEnabled in GetFont and call factory(font) directly. Semantically cleaner; eliminates the observer entirely. Downside: minor per-call overhead since auto-scaled fonts are never reused.

  2. Dual-cache + selective invalidation (Attempt 2): Split cache into _fonts (non-scaled) and _scaledFonts (auto-scaled); add ClearScaledFontCache() called from PageViewController via DI. No observer, no global clear, selective invalidation only.

Cross-pollination surfaced two more ideas: handler-level subscription per-element, and including UIContentSizeCategory in the cache key. All alternatives avoid the observer token leak but none are implemented in the current PR.

Verdict

The PR's logic is correct and the approach is valid. The one required change is fixing the observer token lifecycle to prevent memory leaks. This is a straightforward fix. Once addressed, the PR is ready to merge.

Result: ⚠️ REQUEST CHANGES — Fix observer token lifecycle in FontManager.iOS.cs (store and dispose the NSObject returned by ObserveContentSizeCategoryChanged).


📋 Expand PR Finalization Review

PR #34445 Finalization Review

PR: #34445 - [iOS] Fix Font autoscaling does not work in realtime
Author: HarishwaranVijayakumar
Labels: platform/ios, community ✨, area-controls-label, partner/syncfusion, s/agent-reviewed, s/agent-changes-requested, s/agent-gate-failed
Files Changed: 2 (FontManager.iOS.cs, PageViewController.cs)


Phase 1: Title & Description Review

Title: ⚠️ Minor Improvement Available

Current: [iOS] Fix Font autoscaling does not work in realtime

Assessment: The title is functional and has the correct platform prefix, but "does not work in realtime" reads like a bug description rather than a fix description. It's a bit verbose.

Recommended: [iOS] FontManager: Fix Dynamic Type font scaling not updating in realtime

Reasons:

  • Adds FontManager: component prefix for searchability in git history
  • "not updating" is more concise than "does not work"
  • "Dynamic Type" is the Apple API term — helps future agents searching for Dynamic Type issues

Description: ✅ Good — Minor Addition Needed

Quality Assessment:

Indicator Assessment
NOTE block ✅ Present at top
Root cause ✅ Clear — cache key doesn't change, so stale UIFont is returned
Description of Change ✅ Excellent — file-by-file breakdown with diffhunk links and specific details
Issues Fixed Fixes #34307 present
Platforms Tested ⚠️ Minor inconsistency (see below)

Platforms Tested Inconsistency:
The PR title says [iOS] and the fix is in iOS-specific files, yet Windows and Android are checked in the "Tested" section. The intent seems to be "verified no regression on those platforms" rather than "fix applies there." Consider clarifying this in the description. Also, issue #34307 mentions "iOS, Android" as affected platforms — but this PR only addresses iOS. The Android fix (if needed) would be separate.

Only addition needed:

  • ✅ NOTE block is already present
  • ⚠️ Clarify that Windows/Android checkmarks mean "no regression" not "fix applies"

Overall: The description is well-written with excellent technical depth. Keep it as-is with minor clarification.


Phase 2: Code Review

🔴 Critical Issues

Observer Token Not Retained or Disposed

  • File: src/Core/src/Fonts/FontManager.iOS.cs, line 47
  • Problem: UIApplication.Notifications.ObserveContentSizeCategoryChanged(...) returns an NSObject token. This token must be stored and disposed/removed to avoid memory leaks. As currently written:
    1. Every FontManager instance registers a permanent observer
    2. The closure captures _fonts (which refers to the FontManager instance) — keeping the FontManager alive even after it should be eligible for GC
    3. FontManager does not implement IDisposable, so there is no cleanup path
    4. In tests that create FontManager directly, each test creates another leak
  • This was already flagged by the automated Copilot reviewer (copilot-pull-request-reviewer comment, unresolved)
  • Recommendation: Store the returned token in a field and dispose it. The cleanest approach is to implement IDisposable on FontManager:
// Field:
NSObject? _contentSizeCategoryObserver;

// In constructor:
_contentSizeCategoryObserver = UIApplication.Notifications
    .ObserveContentSizeCategoryChanged((sender, args) => _fonts.Clear());

// Add IDisposable and Dispose():
public void Dispose()
{
    _contentSizeCategoryObserver?.Dispose();
    _contentSizeCategoryObserver = null;
}

Alternatively, if FontManager is always registered as a singleton in DI, the practical leak is bounded to one instance per app — but this still doesn't clean up on app shutdown and would break unit tests that instantiate FontManager directly.


🟡 Suggestions

TraitCollectionDidChange Notification vs. Direct Observer

The PR uses two mechanisms to handle content size category changes:

  1. UIApplication.Notifications.ObserveContentSizeCategoryChanged in FontManager (clears cache)
  2. TraitCollectionDidChange in PageViewController (re-applies fonts to views)

These work together correctly — the cache is cleared first (via the notification center callback), then trait collection change fires and re-fetches fonts from the now-empty cache. Both happen on the main thread, so ordering is reliable. This design is acceptable.

However, the redundancy should be noted: if only one PageViewController is active (most common case), this is fine. If multiple PageViewController instances are active simultaneously (e.g., multiple windows or split views), each will call InvalidateFontsOnContentSizeChanged independently — which is correct behavior (each window's view tree needs to be updated). ✅

Recursion Depth for InvalidateFontsOnContentSizeChanged

The method recurses through the entire visual tree. For deeply nested or very large view hierarchies, this could be slow — but this concern is no different from other visual tree traversals in MAUI (e.g., theme change propagation), and it only fires when the user explicitly changes system font size. Acceptable.

CurrentView as IView — Null Case

PageViewController calls InvalidateFontsOnContentSizeChanged(CurrentView as IView) — if CurrentView is not IView (which should be rare), it silently no-ops. The null guard at the top of InvalidateFontsOnContentSizeChanged handles this correctly. ✅


✅ Looks Good

  • static void InvalidateFontsOnContentSizeChanged is correctly marked static — it has no instance dependencies. ✅
  • The ITextStyle { Font.AutoScalingEnabled: true } pattern check is precise — only views that opted into auto-scaling are updated, avoiding unnecessary mapper invocations. ✅
  • view.Handler.UpdateValue(nameof(ITextStyle.Font)) correctly re-runs the font mapper. ✅
  • view.InvalidateMeasure() ensures layout re-runs after font size change. ✅
  • The previousTraitCollection.PreferredContentSizeCategory != TraitCollection.PreferredContentSizeCategory guard correctly filters TraitCollectionDidChange to only act on content size category changes (not dark mode, display scale, etc.). ✅
  • if (previousTraitCollection is not null && guard prevents NPE. ✅

Summary

Category Assessment
Title ⚠️ Minor improvement recommended
Description ✅ Good quality — keep as-is with minor platform clarification
NOTE block ✅ Present
Issue link Fixes #34307
Code quality 🔴 Critical: observer token not disposed (already flagged)
Logic correctness ✅ The fix logic is sound
Edge cases ✅ Null guards in place

Action Items Before Merge

  1. 🔴 Must Fix: Retain and dispose the NSObject returned by ObserveContentSizeCategoryChanged. The unresolved copilot-pull-request-reviewer comment on this is blocking.
  2. ⚠️ Recommend: Clarify in the description whether Windows/Android checkmarks mean "no regression" vs "fix applies."
  3. Optional: Improve title to include FontManager: component prefix for better git searchability.

@kubaflo kubaflo added the s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates label Mar 14, 2026
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

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

Could you please apply the ai's suggestions?

@HarishwaranVijayakumar
Copy link
Copy Markdown
Contributor Author

Could you please apply the ai's suggestions?

Addressed the AI suggestion.

@kubaflo kubaflo added the s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation label Mar 19, 2026
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 19, 2026

🤖 AI Summary

📊 Expand Full Review97694b0 · Address AI concern
🔍 Pre-Flight — Context & Validation

Issue: #34307 - Font autoscaling does not work in realtime
PR: #34445 - [iOS] Fix Font autoscaling does not work in realtime
Platforms Affected: iOS (and MacCatalyst)
Files Changed: 2 implementation, 0 test

Key Findings

  • Root Cause: FontManager.iOS.cs caches scaled UIFont instances in a ConcurrentDictionary<Font, UIFont>. When the user changes Dynamic Type size at runtime, the Font struct key doesn't change so the same stale UIFont is returned from cache. The fonts never update until app restart.
  • Fix Area 1: FontManager.iOS.cs — Adds observer for UIApplication.Notifications.ObserveContentSizeCategoryChanged to clear _fonts cache when Dynamic Type changes.
  • Fix Area 2: PageViewController.cs — In TraitCollectionDidChange, detects changes to PreferredContentSizeCategory and calls InvalidateFontsOnContentSizeChanged to recursively refresh all autoscaled text elements.
  • Fix Area 3: PageViewController.cs — New static method InvalidateFontsOnContentSizeChanged recursively walks the visual tree via IVisualTreeElement.GetVisualChildren() and calls UpdateValue(nameof(ITextStyle.Font)) + InvalidateMeasure() for every ITextStyle element with AutoScalingEnabled: true.
  • Copilot Review Comment: The NSObject token returned by ObserveContentSizeCategoryChanged is not retained/disposed, which can cause FontManager instance leaks in tests. Should store and dispose the observer token in FontManager's dispose path.
  • No tests exist in this PR — No files in TestCases.HostApp or TestCases.Shared.Tests. Gate phase will be ⚠️ SKIPPED.
  • Issue confirmed reproducible on iOS by MAUI team member; not reproduced on Android.
  • PR author tested on Windows, Android, and iOS (not Mac).

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34445 Clear FontManager cache on ContentSizeCategoryChanged + recursively refresh autoscaled fonts in PageViewController.TraitCollectionDidChange ⏳ PENDING (Gate) FontManager.iOS.cs, PageViewController.cs Original PR

Issue: #34307 - Font autoscaling does not work in realtime
PR: #34445 - [iOS] Fix Font autoscaling does not work in realtime
Platforms Affected: iOS (confirmed), Android listed on issue but not reproduced by validation comment, MacCatalyst claimed by PR description
Files Changed: 2 implementation, 2 API surface, 0 test

Key Findings

  • Issue Font autoscaling does not work in realtime #34307 reports that explicit numeric FontSize values do not react to Dynamic Type changes at runtime, while deprecated named sizes still do.
  • A MAUI validation comment confirms reproduction on iOS and explicitly says it was not reproduced on Android, despite Android being listed in the issue metadata.
  • The PR changes only iOS-specific implementation files plus the corresponding iOS/MacCatalyst PublicAPI.Unshipped.txt files; there are no test files in the PR.
  • Current PR approach: clear the cached scaled fonts in FontManager.iOS.cs when content size category changes, then refresh autoscaled text elements from PageViewController.TraitCollectionDidChange.
  • Prior agent review in PR comments flagged an observer-token leak; the current PR diff now stores the observer token, implements IDisposable, and adds PublicAPI entries for FontManager.Dispose().
  • Review discussion does not surface additional edge cases beyond platform scope and the lack of automated coverage.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34445 Clear cached scaled fonts on Dynamic Type change and recursively refresh autoscaled text from ` PENDING (Gate) src/Core/src/Fonts/FontManager.iOS.cs, src/Core/src/Platform/iOS/PageViewController.cs, src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt, src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt No tests included in PR PageViewController`

🚦 Gate — Test Verification

Gate Result: ⚠️ SKIPPED

Platform: iOS
Mode: N/A — No tests found in PR

Reason: PR #34445 contains no test files. Files changed:

  • src/Core/src/Fonts/FontManager.iOS.cs (implementation only)
  • src/Core/src/Platform/iOS/PageViewController.cs (implementation only)

No files exist in TestCases.HostApp or TestCases.Shared.Tests.

  • Tests FAIL without fix: ⚠️ N/A (no tests to run)
  • Tests PASS with fix: ⚠️ N/A (no tests to run)

Recommendation: Use write-tests-agent to create UI tests for issue #34307 (Font autoscaling realtime update on iOS).


Gate SKIPPEDResult:

Platform: ios
Mode: N/A - No tests found in PR

  • Tests FAIL without N/Afix:
  • Tests PASS with N/Afix:

Reason: PR #34445 changes only implementation and PublicAPI files. It includes no files under TestCases.HostApp or TestCases.Shared.Tests, so there is no PR-supplied UI coverage to verify fail-without-fix / pass-with-fix behavior.

Follow-up: This PR would benefit from issue coverage via write-tests-agent for #34307.


🔧 Fix — Analysis & Comparison

Gate Result: ⚠️ SKIPPED

Platform: iOS
Mode: N/A — No tests found in PR

Reason: PR #34445 contains no test files. Files changed:

  • src/Core/src/Fonts/FontManager.iOS.cs (implementation only)
  • src/Core/src/Platform/iOS/PageViewController.cs (implementation only)

No files exist in TestCases.HostApp or TestCases.Shared.Tests.

  • Tests FAIL without fix: ⚠️ N/A (no tests to run)
  • Tests PASS with fix: ⚠️ N/A (no tests to run)

Recommendation: Use write-tests-agent to create UI tests for issue #34307 (Font autoscaling realtime update on iOS).


Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34445 Clear cached scaled fonts on content size category change and recursively refresh autoscaled text from ` Gate skipped src/Core/src/Fonts/FontManager.iOS.cs, src/Core/src/Platform/iOS/PageViewController.cs, src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt, src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt Current PR includes the observer-token disposal follow-up PageViewController`

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 No NO NEW IDEAS
claude-sonnet-4.6 2 Yes Dynamic Type version stamp plus per-view self-refresh
gpt-5.3-codex 2 No NO NEW IDEAS
gemini-3-pro-preview 2 Yes Native subclass-local TraitCollectionDidChange updates
claude-opus-4.6 3 Yes Handler-base / self-updating handler idea
claude-sonnet-4.6 3 Yes Global handler registry / DynamicTypeManager idea
gpt-5.3-codex 3 Yes IFontManager.FontInvalidated event subscription idea
gemini-3-pro-preview 3 No NO NEW IDEAS

Exhausted: Yes (practical limit reached; all empirical validation was blocked by missing issue-specific tests)
Selected Fix: PR #34445 - simplest current implementation among the explored options, and the prior observer-token lifecycle issue appears addressed in the current diff. The main remaining gap is missing automated coverage.


📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue #34307, 2 implementation files, 0 test files
Gate ⚠️ SKIPPED No tests in PR — cannot verify bug is caught
Try-Fix ✅ COMPLETE 2 attempts, 0 passing (all Blocked — no test available)
Report ✅ COMPLETE

Summary

PR #34445 fixes a real iOS bug where FontManager caches UIFont instances keyed by Font struct. When the user changes Dynamic Type at runtime, the cache key doesn't change, so the stale scaled font is returned until app restart. The PR correctly identifies and addresses this by:

  1. Registering a ContentSizeCategoryChanged observer in FontManager to clear the cache
  2. Detecting PreferredContentSizeCategory changes in PageViewController.TraitCollectionDidChange
  3. Recursively invalidating auto-scaled text elements via InvalidateFontsOnContentSizeChanged

The fix approach is sound and the PR author has manually tested on iOS. However, there is one confirmed code quality issue that needs to be addressed before merge.

Root Cause

FontManager.iOS.cs caches scaled UIFont instances in a ConcurrentDictionary<Font, UIFont>. The Font struct key does not include the current UIContentSizeCategory. When the user changes Dynamic Type, the Font key is unchanged, so _fonts.GetOrAdd(font, factory) returns the stale cached font instead of creating a new one with the updated scaling.

Fix Quality

What's Good:

  • Correct two-pronged approach: clear stale cache + refresh live UI elements
  • Uses ITextStyle.Font.AutoScalingEnabled guard to avoid unnecessary updates
  • Recursive tree walk via IVisualTreeElement.GetVisualChildren() is the right approach for pushing mapper updates
  • Correctly handles previousTraitCollection is not null guard

Issue That Requires Change:

Observer memory leak in FontManager.iOS.csUIApplication.Notifications.ObserveContentSizeCategoryChanged(...) returns a NSObject observer token. The PR discards this token (no variable assigned), which means:

  • The observer is never unregistered
  • The closure captures _fonts (hence FontManager instance), preventing GC
  • In test environments that create FontManager directly, this leaks per-instance

Fix: Store the returned NSObject in an instance field, implement IDisposable, and call _contentSizeCategoryObserver?.Dispose() in Dispose().

Example:

// In class fields:
NSObject? _contentSizeCategoryObserver;

// In constructor:
_contentSizeCategoryObserver = UIApplication.Notifications.ObserveContentSizeCategoryChanged(
    (sender, args) => _fonts.Clear());

// In Dispose():
_contentSizeCategoryObserver?.Dispose();
_contentSizeCategoryObserver = null;

Minor Concern (non-blocking):

  • InvalidateFontsOnContentSizeChanged is called regardless of whether CurrentView itself has auto-scaled fonts at the top level. This is fine for correctness but slightly wasteful if the page has no auto-scaled fonts at all (the tree walk still occurs).

Alternative Approaches Found in Try-Fix

Two alternative approaches were explored (both Blocked due to no tests):

  1. Bypass caching for auto-scaled fonts (Attempt 1): Never cache auto-scaled fonts at all — check font.AutoScalingEnabled in GetFont and call factory(font) directly. Semantically cleaner; eliminates the observer entirely. Downside: minor per-call overhead since auto-scaled fonts are never reused.

  2. Dual-cache + selective invalidation (Attempt 2): Split cache into _fonts (non-scaled) and _scaledFonts (auto-scaled); add ClearScaledFontCache() called from PageViewController via DI. No observer, no global clear, selective invalidation only.

Cross-pollination surfaced two more ideas: handler-level subscription per-element, and including UIContentSizeCategory in the cache key. All alternatives avoid the observer token leak but none are implemented in the current PR.

Verdict

The PR's logic is correct and the approach is valid. The one required change is fixing the observer token lifecycle to prevent memory leaks. This is a straightforward fix. Once addressed, the PR is ready to merge.

Result: ⚠️ REQUEST CHANGES — Fix observer token lifecycle in FontManager.iOS.cs (store and dispose the NSObject returned by ObserveContentSizeCategoryChanged).


Final Recommendation: REQUEST CHANGES##

Phase Status

Phase Status Notes
Pre-Flight COMPLETE Issue #34307, 2 implementation files, 2 PublicAPI files, 0 test files
SKIPPED No PR-supplied UI tests to verify fail-without-fix / pass-with-fix behavior Gate
Try-Fix COMPLETE 5 independent candidates explored; all blocked by Issue34307 matching 0 tests
Report COMPLETE

Summary

PR #34445 addresses a real iOS Dynamic Type regression and the current implementation is reasonable: it clears stale scaled fonts in FontManager and refreshes autoscaled text when the preferred content size category changes. The earlier review concern about leaking the notification observer appears to be fixed in the current diff by storing and disposing the observer token.

However, the PR still lacks automated issue coverage. The Gate phase could not verify the bug because the PR contains no HostApp / shared UI test for Issue34307, and every try-fix attempt hit the same blocker: the iOS runner discovered 0 of 0 NUnit tests and printed No test matches the given testcase filter 'Issue34307'. That means none of the candidate fixes, including the current PR, were empirically validated by issue-specific UI coverage.

Root Cause

On iOS, FontManager caches scaled UIFont instances keyed by the MAUI Font value. When the system Dynamic Type category changes at runtime, that cache can continue returning a font built for the old content-size category unless MAUI invalidates the cached entry and pushes a font refresh back through the active text elements.

Fix Quality

The current PR is the best option among the explored candidates because it is more localized and less invasive than the alternatives explored in try-fix. The alternative ideas either widened the native surface area, introduced new tracking infrastructure, or relied on UIKit-only behavior that still could not be verified here.

The blocking issue is test coverage, not an obvious logic flaw in the latest PR diff. Because the skill's gate could not be satisfied, this review cannot recommend approval yet.


📋 Expand PR Finalization Review

PR #34445 Finalization Review

PR: #34445 - [iOS] Fix Font autoscaling does not work in realtime
Author: HarishwaranVijayakumar
Labels: platform/ios, community ✨, area-controls-label, partner/syncfusion, s/agent-reviewed, s/agent-changes-requested, s/agent-gate-failed
Files Changed: 2 (FontManager.iOS.cs, PageViewController.cs)


Phase 1: Title & Description Review

Title: ⚠️ Minor Improvement Available

Current: [iOS] Fix Font autoscaling does not work in realtime

Assessment: The title is functional and has the correct platform prefix, but "does not work in realtime" reads like a bug description rather than a fix description. It's a bit verbose.

Recommended: [iOS] FontManager: Fix Dynamic Type font scaling not updating in realtime

Reasons:

  • Adds FontManager: component prefix for searchability in git history
  • "not updating" is more concise than "does not work"
  • "Dynamic Type" is the Apple API term — helps future agents searching for Dynamic Type issues

Description: ✅ Good — Minor Addition Needed

Quality Assessment:

Indicator Assessment
NOTE block ✅ Present at top
Root cause ✅ Clear — cache key doesn't change, so stale UIFont is returned
Description of Change ✅ Excellent — file-by-file breakdown with diffhunk links and specific details
Issues Fixed Fixes #34307 present
Platforms Tested ⚠️ Minor inconsistency (see below)

Platforms Tested Inconsistency:
The PR title says [iOS] and the fix is in iOS-specific files, yet Windows and Android are checked in the "Tested" section. The intent seems to be "verified no regression on those platforms" rather than "fix applies there." Consider clarifying this in the description. Also, issue #34307 mentions "iOS, Android" as affected platforms — but this PR only addresses iOS. The Android fix (if needed) would be separate.

Only addition needed:

  • ✅ NOTE block is already present
  • ⚠️ Clarify that Windows/Android checkmarks mean "no regression" not "fix applies"

Overall: The description is well-written with excellent technical depth. Keep it as-is with minor clarification.


Phase 2: Code Review

🔴 Critical Issues

Observer Token Not Retained or Disposed

  • File: src/Core/src/Fonts/FontManager.iOS.cs, line 47
  • Problem: UIApplication.Notifications.ObserveContentSizeCategoryChanged(...) returns an NSObject token. This token must be stored and disposed/removed to avoid memory leaks. As currently written:
    1. Every FontManager instance registers a permanent observer
    2. The closure captures _fonts (which refers to the FontManager instance) — keeping the FontManager alive even after it should be eligible for GC
    3. FontManager does not implement IDisposable, so there is no cleanup path
    4. In tests that create FontManager directly, each test creates another leak
  • This was already flagged by the automated Copilot reviewer (copilot-pull-request-reviewer comment, unresolved)
  • Recommendation: Store the returned token in a field and dispose it. The cleanest approach is to implement IDisposable on FontManager:
// Field:
NSObject? _contentSizeCategoryObserver;

// In constructor:
_contentSizeCategoryObserver = UIApplication.Notifications
    .ObserveContentSizeCategoryChanged((sender, args) => _fonts.Clear());

// Add IDisposable and Dispose():
public void Dispose()
{
    _contentSizeCategoryObserver?.Dispose();
    _contentSizeCategoryObserver = null;
}

Alternatively, if FontManager is always registered as a singleton in DI, the practical leak is bounded to one instance per app — but this still doesn't clean up on app shutdown and would break unit tests that instantiate FontManager directly.


🟡 Suggestions

TraitCollectionDidChange Notification vs. Direct Observer

The PR uses two mechanisms to handle content size category changes:

  1. UIApplication.Notifications.ObserveContentSizeCategoryChanged in FontManager (clears cache)
  2. TraitCollectionDidChange in PageViewController (re-applies fonts to views)

These work together correctly — the cache is cleared first (via the notification center callback), then trait collection change fires and re-fetches fonts from the now-empty cache. Both happen on the main thread, so ordering is reliable. This design is acceptable.

However, the redundancy should be noted: if only one PageViewController is active (most common case), this is fine. If multiple PageViewController instances are active simultaneously (e.g., multiple windows or split views), each will call InvalidateFontsOnContentSizeChanged independently — which is correct behavior (each window's view tree needs to be updated). ✅

Recursion Depth for InvalidateFontsOnContentSizeChanged

The method recurses through the entire visual tree. For deeply nested or very large view hierarchies, this could be slow — but this concern is no different from other visual tree traversals in MAUI (e.g., theme change propagation), and it only fires when the user explicitly changes system font size. Acceptable.

CurrentView as IView — Null Case

PageViewController calls InvalidateFontsOnContentSizeChanged(CurrentView as IView) — if CurrentView is not IView (which should be rare), it silently no-ops. The null guard at the top of InvalidateFontsOnContentSizeChanged handles this correctly. ✅


✅ Looks Good

  • static void InvalidateFontsOnContentSizeChanged is correctly marked static — it has no instance dependencies. ✅
  • The ITextStyle { Font.AutoScalingEnabled: true } pattern check is precise — only views that opted into auto-scaling are updated, avoiding unnecessary mapper invocations. ✅
  • view.Handler.UpdateValue(nameof(ITextStyle.Font)) correctly re-runs the font mapper. ✅
  • view.InvalidateMeasure() ensures layout re-runs after font size change. ✅
  • The previousTraitCollection.PreferredContentSizeCategory != TraitCollection.PreferredContentSizeCategory guard correctly filters TraitCollectionDidChange to only act on content size category changes (not dark mode, display scale, etc.). ✅
  • if (previousTraitCollection is not null && guard prevents NPE. ✅

Summary

Category Assessment
Title ⚠️ Minor improvement recommended
Description ✅ Good quality — keep as-is with minor platform clarification
NOTE block ✅ Present
Issue link Fixes #34307
Code quality 🔴 Critical: observer token not disposed (already flagged)
Logic correctness ✅ The fix logic is sound
Edge cases ✅ Null guards in place

Action Items Before Merge

  1. 🔴 Must Fix: Retain and dispose the NSObject returned by ObserveContentSizeCategoryChanged. The unresolved copilot-pull-request-reviewer comment on this is blocking.
  2. ⚠️ Recommend: Clarify in the description whether Windows/Android checkmarks mean "no regression" vs "fix applies."
  3. Optional: Improve title to include FontManager: component prefix for better git searchability.

Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

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

Could you please apply the AI's suggestions?

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 28, 2026

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

@HarishwaranVijayakumar
Copy link
Copy Markdown
Contributor Author

Could you please apply the AI's suggestions?

Addressed the concerns.

@sheiksyedm
Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-controls-label Label, Span community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/ios s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Font autoscaling does not work in realtime

7 participants