[iOS] Fix Font autoscaling does not work in realtime#34445
[iOS] Fix Font autoscaling does not work in realtime#34445HarishwaranVijayakumar wants to merge 6 commits intodotnet:mainfrom
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34445Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34445" |
|
/azp run maui-pr-uitests,maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
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
FontManagerfont cache whenContentSizeCategoryChangedis raised. - Detect preferred content size category changes in
PageViewController.TraitCollectionDidChangeand 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. |
🤖 AI Summary📊 Expand Full Review —
|
| # | 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 |
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 | 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:
- Registering a
ContentSizeCategoryChangedobserver inFontManagerto clear the cache - Detecting
PreferredContentSizeCategorychanges inPageViewController.TraitCollectionDidChange - 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.AutoScalingEnabledguard to avoid unnecessary updates - Recursive tree walk via
IVisualTreeElement.GetVisualChildren()is the right approach for pushing mapper updates - Correctly handles
previousTraitCollection is not nullguard
Issue That Requires Change:
Observer memory leak in FontManager.iOS.cs — UIApplication.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(henceFontManagerinstance), preventing GC - In test environments that create
FontManagerdirectly, 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):
InvalidateFontsOnContentSizeChangedis called regardless of whetherCurrentViewitself 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):
-
Bypass caching for auto-scaled fonts (Attempt 1): Never cache auto-scaled fonts at all — check
font.AutoScalingEnabledinGetFontand callfactory(font)directly. Semantically cleaner; eliminates the observer entirely. Downside: minor per-call overhead since auto-scaled fonts are never reused. -
Dual-cache + selective invalidation (Attempt 2): Split cache into
_fonts(non-scaled) and_scaledFonts(auto-scaled); addClearScaledFontCache()called fromPageViewControllervia 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: 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 |
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 anNSObjecttoken. This token must be stored and disposed/removed to avoid memory leaks. As currently written:- Every
FontManagerinstance registers a permanent observer - The closure captures
_fonts(which refers to theFontManagerinstance) — keeping theFontManageralive even after it should be eligible for GC FontManagerdoes not implementIDisposable, so there is no cleanup path- In tests that create
FontManagerdirectly, each test creates another leak
- Every
- This was already flagged by the automated Copilot reviewer (
copilot-pull-request-reviewercomment, unresolved) - Recommendation: Store the returned token in a field and dispose it. The cleanest approach is to implement
IDisposableonFontManager:
// 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:
UIApplication.Notifications.ObserveContentSizeCategoryChangedinFontManager(clears cache)TraitCollectionDidChangeinPageViewController(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 InvalidateFontsOnContentSizeChangedis correctly markedstatic— 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.PreferredContentSizeCategoryguard correctly filtersTraitCollectionDidChangeto 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 | |
| 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
- 🔴 Must Fix: Retain and dispose the
NSObjectreturned byObserveContentSizeCategoryChanged. The unresolvedcopilot-pull-request-reviewercomment on this is blocking. ⚠️ Recommend: Clarify in the description whether Windows/Android checkmarks mean "no regression" vs "fix applies."- Optional: Improve title to include
FontManager:component prefix for better git searchability.
kubaflo
left a comment
There was a problem hiding this comment.
Could you please apply the ai's suggestions?
Addressed the AI suggestion. |
🤖 AI Summary📊 Expand Full Review —
|
| # | 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
FontSizevalues 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.txtfiles; there are no test files in the PR. - Current PR approach: clear the cached scaled fonts in
FontManager.iOS.cswhen content size category changes, then refresh autoscaled text elements fromPageViewController.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 forFontManager.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 | 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:
- Registering a
ContentSizeCategoryChangedobserver inFontManagerto clear the cache - Detecting
PreferredContentSizeCategorychanges inPageViewController.TraitCollectionDidChange - 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.AutoScalingEnabledguard to avoid unnecessary updates - Recursive tree walk via
IVisualTreeElement.GetVisualChildren()is the right approach for pushing mapper updates - Correctly handles
previousTraitCollection is not nullguard
Issue That Requires Change:
Observer memory leak in FontManager.iOS.cs — UIApplication.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(henceFontManagerinstance), preventing GC - In test environments that create
FontManagerdirectly, 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):
InvalidateFontsOnContentSizeChangedis called regardless of whetherCurrentViewitself 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):
-
Bypass caching for auto-scaled fonts (Attempt 1): Never cache auto-scaled fonts at all — check
font.AutoScalingEnabledinGetFontand callfactory(font)directly. Semantically cleaner; eliminates the observer entirely. Downside: minor per-call overhead since auto-scaled fonts are never reused. -
Dual-cache + selective invalidation (Attempt 2): Split cache into
_fonts(non-scaled) and_scaledFonts(auto-scaled); addClearScaledFontCache()called fromPageViewControllervia 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: 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 |
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 anNSObjecttoken. This token must be stored and disposed/removed to avoid memory leaks. As currently written:- Every
FontManagerinstance registers a permanent observer - The closure captures
_fonts(which refers to theFontManagerinstance) — keeping theFontManageralive even after it should be eligible for GC FontManagerdoes not implementIDisposable, so there is no cleanup path- In tests that create
FontManagerdirectly, each test creates another leak
- Every
- This was already flagged by the automated Copilot reviewer (
copilot-pull-request-reviewercomment, unresolved) - Recommendation: Store the returned token in a field and dispose it. The cleanest approach is to implement
IDisposableonFontManager:
// 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:
UIApplication.Notifications.ObserveContentSizeCategoryChangedinFontManager(clears cache)TraitCollectionDidChangeinPageViewController(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 InvalidateFontsOnContentSizeChangedis correctly markedstatic— 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.PreferredContentSizeCategoryguard correctly filtersTraitCollectionDidChangeto 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 | |
| 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
- 🔴 Must Fix: Retain and dispose the
NSObjectreturned byObserveContentSizeCategoryChanged. The unresolvedcopilot-pull-request-reviewercomment on this is blocking. ⚠️ Recommend: Clarify in the description whether Windows/Android checkmarks mean "no regression" vs "fix applies."- Optional: Improve title to include
FontManager:component prefix for better git searchability.
kubaflo
left a comment
There was a problem hiding this comment.
Could you please apply the AI's suggestions?
|
|
97694b0 to
3b432e3
Compare
Addressed the concerns. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
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
Root Cause of the issue
FontManager.iOS.cscaches scaledUIFontinstances in aConcurrentDictionary<Font, UIFont>. The cache key is theFontstruct (family, size, weight, etc.).Once cached, the sameUIFontis returned on every subsequent request — even after the user changes the Dynamic Type size — because theFontstruct key hasn't changed.Description of Change
Dynamic Type font scaling improvements:
src/Core/src/Fonts/FontManager.iOS.cs: Added an observer forContentSizeCategoryChangednotifications 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: InTraitCollectionDidChange, added logic to detect changes in the preferred content size category and callInvalidateFontsOnContentSizeChangedto update fonts across all text elements in the current view.src/Core/src/Platform/iOS/PageViewController.cs: Introduced theInvalidateFontsOnContentSizeChangedmethod, 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
Before.mov
After.mov