Skip to content

[iOS] Fix Picker CharacterSpacing lost after item selection when Title is set#34974

Open
SyedAbdulAzeemSF4852 wants to merge 2 commits intodotnet:mainfrom
SyedAbdulAzeemSF4852:fix/picker-charspacing-lost-on-select
Open

[iOS] Fix Picker CharacterSpacing lost after item selection when Title is set#34974
SyedAbdulAzeemSF4852 wants to merge 2 commits intodotnet:mainfrom
SyedAbdulAzeemSF4852:fix/picker-charspacing-lost-on-select

Conversation

@SyedAbdulAzeemSF4852
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 a Picker has both a Title and a non-zero CharacterSpacing, the character spacing is silently lost as soon as the user selects an item from the picker, or when SelectedIndex is changed programmatically. The spacing is visible before selection (applied to the placeholder/title text), but after any item is selected, the selected item renders without spacing.

Root Cause

  • Regression: Introduced by PR [iOS] Fixed picker title's color #20205
  • PR [iOS] Fixed picker title's color #20205 changed iOS Picker to apply CharacterSpacing via AttributedPlaceholder so the Title text respects kern. When the user selects an item, the code assigns a plain string to platformPicker.Text. iOS internally creates a zero-kern AttributedText from that plain string and simultaneously hides AttributedPlaceholder, since iOS never shows a placeholder when the text field has a value. This means the kern that was applied to the placeholder is gone, and no kern is applied to the selected item text — resulting in CharacterSpacing being silently lost after selection.

Description of Change

  • After platformPicker.Text is set to the selected item's string, UpdateCharacterSpacing is now explicitly called to re-apply kern to AttributedText. This is done in two places: in UpdatePickerFromPickerSource (the Done-button code path in PickerHandler.iOS.cs) and in UpdatePicker (the programmatic SelectedIndex change path in PickerExtensions.cs), ensuring CharacterSpacing survives item selection regardless of how the selection is triggered.

Issues Fixed

Fixes #34971

Validated the behaviour in the following platforms

  • Windows
  • Android
  • iOS
  • Mac

Output

Before After
Before.mov
After.mov

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 15, 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 -- 34974

Or

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

@dotnet-policy-service dotnet-policy-service Bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Apr 15, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review April 15, 2026 10:57
Copilot AI review requested due to automatic review settings April 15, 2026 10:57
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 an iOS/MacCatalyst regression where Picker.CharacterSpacing is lost after an item is selected when Title is set, by re-applying kerning after the native text is assigned.

Changes:

  • Re-apply CharacterSpacing after platformPicker.Text is set during selection updates (both programmatic and “Done” selection paths).
  • Add an iOS device test to validate CharacterSpacing is preserved across SelectedIndex updates.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/Core/tests/DeviceTests/Handlers/Picker/PickerHandlerTests.iOS.cs Adds a device test asserting character spacing is preserved when SelectedIndex changes programmatically.
src/Core/src/Platform/iOS/PickerExtensions.cs Ensures UpdatePicker always re-applies character spacing after setting Text.
src/Core/src/Handlers/Picker/PickerHandler.iOS.cs Re-applies character spacing after assigning Text in the “Done button” selection flow.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 15, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 15, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 15, 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 try the ai's suggestions?

@dotnet dotnet deleted a comment from MauiBot Apr 23, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 23, 2026
@MauiBot MauiBot added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR labels Apr 23, 2026
@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues and removed s/agent-approved AI agent recommends approval - PR fix is correct and optimal labels Apr 24, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 24, 2026
@MauiBot MauiBot added s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates labels Apr 24, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 24, 2026
@MauiBot MauiBot added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Apr 24, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 24, 2026
@MauiBot MauiBot added s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates labels Apr 24, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 25, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 26, 2026
PureWeen pushed a commit that referenced this pull request Apr 30, 2026
…ability (#35133)

<!-- Please let the below note in for people that find this PR -->
> [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

> **Depends on #35136** (pipeline category detection — should merge
first)

## What this does

Two things:

### 1. UI test category detection in PR review

During the PR review workflow, Step 0.5 detects which UI test categories
the PR impacts and writes the result to the AI summary comment. This
gives reviewers visibility into which UI tests are relevant.

**Detection** reuses the 3-tier script from #35136 (test attributes →
source paths → AI reasoning).

**AI summary** shows a new 🧪 UI Tests section with detected categories
before the gate section.

### 2. Gate reliability fixes

Multiple fixes to make the gate (`verify-tests-fail.ps1`) more
deterministic:

| Fix | Problem it solves |
|-----|-------------------|
| **Absolute path resolution** | Gate scripts not found on Linux CI
agents (`Resolve-Path`, `GetFullPath`) |
| **File existence check** | Instant cryptic failure when verify script
is missing — now logs clear error |
| **3x retry on ENV ERROR** | Emulator timeouts, ADB failures, app
crashes — transient issues that pass on retry |
| **Strip bad report blocks** | Old verify script produces `Passed:
False` with empty counts — stripped instead of shown |
| **Gate log in fallback** | When report is missing, shows last 20 lines
of gate output instead of just `❌ FAILED / Platform: IOS` |

## Files

| File | Changes |
|------|---------|
| `.github/scripts/Review-PR.ps1` | Step 0.5 category detection + all 5
gate fixes |
| `.github/scripts/post-ai-summary-comment.ps1` | Add `uitests` phase to
render detected categories |
| `.github/pr-review/pr-preflight.md` | Step 7: AI identifies impacted
UI test categories |

## Validation — PR reviewer builds (Apr 26)

10 builds against real PRs — all succeeded ✅. Category detection shown
in AI summary comment.

| PR | Categories Detected | Build | AI Summary |
|----|-------------------|-------|------------|
| #35037 (WebView theme) | `ViewBaseTests,WebView` |
[13940071](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940071)
|
[comment](#35037 (comment))
|
| #35031 (Shell memory leak) | `Shell` |
[13940072](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940072)
|
[comment](#35031 (comment))
|
| #35020 (XAML Hot Reload) | _(none — XAML only)_ |
[13940073](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940073)
| ✅ Shows "No UI test categories" |
| #35008 (Shell SearchHandler) | `Shell` |
[13940074](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940074)
| ✅ |
| #34997 (RadioButton gradient) | `RadioButton,ViewBaseTests` |
[13940075](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940075)
| ✅ |
| #34980 (DatePicker rotation) | `ViewBaseTests` |
[13940076](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940076)
| ✅ |
| #34974 (Picker CharacterSpacing) | `ViewBaseTests` |
[13940077](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940077)
| ✅ |
| #34923 (SwipeView threshold) | `SwipeView,ViewBaseTests` |
[13940078](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940078)
| ✅ |
| #34907 (CollectionView ScrollTo) | `CollectionView` |
[13940079](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940079)
| ✅ |
| #34845 (RefreshView binding) | `RefreshView,ViewBaseTests` |
[13940080](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940080)
| ✅ |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet dotnet deleted a comment from MauiBot May 2, 2026
Copy link
Copy Markdown
Collaborator

@MauiBot MauiBot left a comment

Choose a reason for hiding this comment

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

Expert Review — 1 findings

See inline comments for details.

@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
MauiBot
MauiBot previously requested changes May 3, 2026
Copy link
Copy Markdown
Collaborator

@MauiBot MauiBot left a comment

Choose a reason for hiding this comment

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

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #1 automatically generated candidates and selected try-fix-1 as the strongest fix.

Why: try-fix-1 passed all 4 regression tests empirically using a SetTextWithCharacterSpacing helper that atomically constructs NSAttributedString with KerningAdjustment instead of the PR's fragile read-modify-write pattern. It also covers the OnEditing handler gap the PR missed and avoids the MacCatalyst test crash entirely.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-1`)
diff --git a/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs b/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs
index a54299d290..11fe557578 100644
--- a/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs
+++ b/src/Core/src/Handlers/Picker/PickerHandler.iOS.cs
@@ -255,7 +255,7 @@ namespace Microsoft.Maui.Handlers
 			if (VirtualView == null || PlatformView == null || pickerSource == null)
 				return;
 
-			PlatformView.Text = VirtualView.GetItem(pickerSource.SelectedIndex);
+			PlatformView.SetTextWithCharacterSpacing(VirtualView.GetItem(pickerSource.SelectedIndex), VirtualView.CharacterSpacing);
 			VirtualView.SelectedIndex = pickerSource.SelectedIndex;
 		}
 
@@ -423,7 +423,7 @@ namespace Microsoft.Maui.Handlers
 				// Reset the TextField's Text so it appears as if typing with a keyboard does not work.
 				var selectedIndex = virtualView.SelectedIndex;
 
-				platformView.Text = virtualView.GetItem(selectedIndex);
+				platformView.SetTextWithCharacterSpacing(virtualView.GetItem(selectedIndex), virtualView.CharacterSpacing);
 
 				// Also clears the undo stack (undo/redo possible on iPads)
 				platformView.UndoManager?.RemoveAllActions();
diff --git a/src/Core/src/Platform/iOS/PickerExtensions.cs b/src/Core/src/Platform/iOS/PickerExtensions.cs
index c2de04d1fd..13e429ae28 100644
--- a/src/Core/src/Platform/iOS/PickerExtensions.cs
+++ b/src/Core/src/Platform/iOS/PickerExtensions.cs
@@ -1,6 +1,7 @@
 #nullable enable
 using System;
 using Foundation;
+using UIKit;
 
 namespace Microsoft.Maui.Platform
 {
@@ -36,18 +37,45 @@ namespace Microsoft.Maui.Platform
 			platformPicker.UpdateAttributedPlaceholder(new NSAttributedString(picker.Title ?? string.Empty, null, picker?.TitleColor?.ToPlatform()));
 		}
 
+		/// <summary>
+		/// Sets text on a picker using AttributedText with kern to avoid iOS clearing
+		/// character spacing when plain Text is assigned.
+		/// </summary>
+		/// <remarks>
+		/// Unlike UpdateCharacterSpacing (which reads back AttributedText and adds kern),
+		/// this creates a fresh NSAttributedString to work around an iOS issue where
+		/// reading AttributedText immediately after setting Text doesn't reliably
+		/// preserve kern in all handler lifecycle contexts.
+		/// </remarks>
+		internal static void SetTextWithCharacterSpacing(this MauiPicker platformPicker, string text, double characterSpacing)
+		{
+			if (characterSpacing == 0)
+			{
+				platformPicker.Text = text;
+				return;
+			}
+
+			var attributes = new UIKit.UIStringAttributes
+			{
+				KerningAdjustment = (float)characterSpacing
+			};
+			platformPicker.AttributedText = new NSAttributedString(text ?? string.Empty, attributes);
+		}
+
 		internal static void UpdatePicker(this MauiPicker platformPicker, IPicker picker, int? newSelectedIndex = null)
 		{
 			var selectedIndex = newSelectedIndex ?? picker.SelectedIndex;
 
 			if (selectedIndex != -1)
 			{
-				platformPicker.Text = picker.GetItem(selectedIndex);
+				platformPicker.SetTextWithCharacterSpacing(picker.GetItem(selectedIndex), picker.CharacterSpacing);
 			}
 			else
 			{
 				platformPicker.Text = null;
 				platformPicker.UpdatePickerTitle(picker);
+				// Applies character spacing to the placeholder when no item is selected
+				platformPicker.UpdateCharacterSpacing(picker);
 			}
 
 			var pickerView = platformPicker.UIPickerView;

@dotnet dotnet deleted a comment from MauiBot May 8, 2026
@kubaflo kubaflo dismissed MauiBot’s stale review May 8, 2026 01:22

Resetting for re-review

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

Labels

area-controls-picker Picker 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-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] Picker loses CharacterSpacing after item selection when Title is set

6 participants