Description
I'm seeing vey inconstant CursorPoint calculations for entries in iOS devices.
I have the following behavior:
using System.Text;
namespace br.com.lb.lilaapp.Behaviors
{
public class DualMaskedBehavior : Behavior<Entry>
{
private bool _isUpdating;
protected override void OnAttachedTo(Entry bindable)
{
base.OnAttachedTo(bindable);
bindable.TextChanged += OnTextChangedAsync;
}
protected override void OnDetachingFrom(Entry bindable)
{
base.OnDetachingFrom(bindable);
bindable.TextChanged -= OnTextChangedAsync;
}
private async void OnTextChangedAsync(object sender, TextChangedEventArgs e)
{
if (_isUpdating || sender is not Entry entry)
return;
string oldText = e.OldTextValue ?? string.Empty;
string newText = e.NewTextValue ?? string.Empty;
string digits = ExtractDigits(newText);
string masked = ApplyMask(digits);
if (entry.Text == masked)
return;
_isUpdating = true;
int oldCursor = entry.CursorPosition;
entry.Text = masked;
// Deixa o ciclo de UI aplicar o novo texto antes de mexer no cursor
await Task.Yield();
#if ANDROID
entry.CursorPosition = CalculateCursorPositionAndroid(oldCursor, oldText, masked);
#endif
#if IOS
entry.CursorPosition = CalculateCursorPositionIos(oldText, masked);
#endif
entry.SelectionLength = 0;
_isUpdating = false;
}
private static string ExtractDigits(string input) =>
new string(input.Where(char.IsDigit).ToArray());
private static string ApplyMask(string digits)
{
if (digits.Length <= 11)
return ApplyCpfMask(digits);
if (digits.Length <= 15)
return ApplyCnsMask(digits);
return digits;
}
private static string ApplyCpfMask(string digits)
{
if (digits.Length > 11)
digits = digits[..11];
var sb = new StringBuilder();
for (int i = 0; i < digits.Length; i++)
{
sb.Append(digits[i]);
if ((i == 2 || i == 5) && i != digits.Length - 1)
sb.Append('.');
if (i == 8 && i != digits.Length - 1)
sb.Append('-');
}
return sb.ToString();
}
private static string ApplyCnsMask(string digits)
{
return digits;
}
private static int CalculateCursorPositionAndroid(int oldCursor, string oldText, string newText)
{
if (newText.Length > oldText.Length)
{
int newCursorPosition = oldCursor + 1;
if (justPassedOverAMaskChar(newText, newCursorPosition))
{
newCursorPosition++;
}
return newCursorPosition;
}
else
{
int newCursorPosition = Math.Max(oldCursor - 1, 0);
if (previousCharIsAMaskChar(oldText, newCursorPosition))
{
newCursorPosition--;
}
return newCursorPosition;
}
}
private static bool previousCharIsAMaskChar(string text, int newCursorPosition)
{
return newCursorPosition > 0 && newCursorPosition <= text.Length && !char.IsDigit(text[newCursorPosition - 1]);
}
private static bool justPassedOverAMaskChar(string text, int newCursorPosition)
{
return newCursorPosition < text.Length && Char.IsDigit(text[newCursorPosition]) && !Char.IsDigit(text[newCursorPosition - 1]);
}
private static int CalculateCursorPositionIos(string oldText, string newText)
{
var oldTextArray = oldText.ToCharArray();
var newTextArray = newText.ToCharArray();
int cursorPosition = newText.Length;
if (MasksJustSwitched(oldText, newText))
return cursorPosition;
bool newTextIsBigger = newText.Length > oldText.Length;
for (int charPosition = 0; charPosition < oldTextArray.Length; charPosition++)
{
if (lastDigitWasBackspaced(charPosition, newTextArray))
{
cursorPosition = newText.Length;
break;
}
if (newTextArray[charPosition] != oldTextArray[charPosition] && Char.IsDigit(oldTextArray[charPosition]))
{
cursorPosition = newTextIsBigger ? charPosition + 1 : charPosition;
break;
}
}
return cursorPosition;
}
private static bool MasksJustSwitched(string oldchar, string newChar)
{
return (newChar.Length == 12 && oldchar.Length == 14)
|| (oldchar.Length == 12 && newChar.Length == 14);
}
private static bool lastDigitWasBackspaced(int charPosition, char[] newTextArray)
{
return charPosition >= newTextArray.Length;
}
}
}`
This code apply conditional masking depending on the number of characters typed in the entry.
This line:
int oldCursor = entry.CursorPosition;
Is pretty much precise in Android, capturing the correct cursor position regardless of the string composition.
But on iOS devices it is chaotic. Sometimes it returns 0, sometimes a random number; specially after it stops being a digits-only charset. That's why I need a special treatment for iOS cases where I try to "guess" the cursor position based on the differences between the old and the new strings.
The erratic behavior must follow some pattern, but I could not identify it.
### Steps to Reproduce
1. Create a custom entry:
public class RoundedEntryNoEmojis : RoundedEntry
{
}
2. Give it a special treatment on Android:
#if ANDROID
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("DisableEmojiCompat", (handler, view) =>
{
if (view is RoundedEntryNoEmojis)
{
handler.PlatformView.EmojiCompatEnabled = false;
}
});
#endif
}
3 . Created a custom behavior as above
4. Apply the behavior to the custom entry. Example:
5.
<custom:RoundedEntryNoEmojis Placeholder="CPF"
Text="{Binding CPForCNS}"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
HeightRequest="50"
Keyboard="Numeric"
FontFamily="RegularFont"
TextColor="{StaticResource DefaultTextColor}"
PlaceholderColor="{StaticResource DefaultPlaceholderColor}"
HorizontalOptions="FillAndExpand"
FontSize="{Binding FontSmall}"
IsEnabled="{Binding Loading, Converter={StaticResource Key=negative}}"
MaxLength="15">
<custom:RoundedEntry.Behaviors>
<behavior:DualMaskedBehavior />
</custom:RoundedEntry.Behaviors>
</custom:RoundedEntryNoEmojis>
5. Run on iOS emulator, debug the code and insert a breaking on the entry.CursorPosition calculation.
It should always capture the correct cursor position, but seldom it does.
### Link to public reproduction project repository
_No response_
### Version with bug
9.0.82 SR8.2
### Is this a regression from previous behavior?
Not sure, did not test other versions
### Last version that worked well
_No response_
### Affected platforms
iOS
### Affected platform versions
iOS 17.2
### Did you find any workaround?
I'm comparing the old and new entry strings and guessing the cursor position, but it is not 100% effective.
### Relevant log output
```shell
Description
I'm seeing vey inconstant CursorPoint calculations for entries in iOS devices.
I have the following behavior:
#if ANDROID
#endif
}