File: Handlers\Editor\EditorHandler.iOS.cs
Web Access
Project: src\src\Core\src\Core.csproj (Microsoft.Maui)
using System;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Graphics;
using UIKit;
 
namespace Microsoft.Maui.Handlers
{
	public partial class EditorHandler : ViewHandler<IEditor, MauiTextView>
	{
		readonly MauiTextViewEventProxy _proxy = new();
 
		protected override MauiTextView CreatePlatformView()
		{
			var platformEditor = new MauiTextView();
 
#if !MACCATALYST
			var accessoryView = new MauiDoneAccessoryView();
			accessoryView.SetDataContext(this);
			accessoryView.SetDoneClicked(OnDoneClicked);
			platformEditor.InputAccessoryView = accessoryView;
#endif
 
			return platformEditor;
		}
 
#if !MACCATALYST
		static void OnDoneClicked(object sender)
		{
			if (sender is IEditorHandler handler)
			{
				handler.PlatformView.ResignFirstResponder();
				handler.VirtualView.Completed();
			}
		}
#endif
 
		public override void SetVirtualView(IView view)
		{
			base.SetVirtualView(view);
 
			_proxy.SetVirtualView(PlatformView);
		}
 
		protected override void ConnectHandler(MauiTextView platformView)
		{
			_proxy.Connect(VirtualView, platformView);
		}
 
		protected override void DisconnectHandler(MauiTextView platformView)
		{
			_proxy.Disconnect(platformView);
		}
 
		public override bool NeedsContainer
		{
			get
			{
				// The layout of the Editor behaves differently on iOS 16 and earlier versions when the size or scale changes at runtime.
				// https://github.com/dotnet/maui/issues/25581 - The content height gradually increases when scaling down the Editor, indicating improper handling of sizing.
				// It appears that iOS 17.0 manages this correctly.
				// To ensure consistent behavior that matches iOS 17.0, we wrap the Editor in a container on iOS 16 and earlier versions.
				if (!OperatingSystem.IsIOSVersionAtLeast(17) && !OperatingSystem.IsMacCatalyst())
				{
					return true;
				}
 
				return base.NeedsContainer;
			}
		}
 
		public override Size GetDesiredSize(double widthConstraint, double heightConstraint)
		{
			if (double.IsInfinity(widthConstraint) || double.IsInfinity(heightConstraint))
			{
				// If we drop an infinite value into base.GetDesiredSize for the Editor, we'll
				// get an exception; it doesn't know what do to with it. So instead we'll size
				// it to fit its current contents and use those values to replace infinite constraints
 
				var sizeThatFits = PlatformView.SizeThatFits(new CGSize(widthConstraint, heightConstraint));
 
				if (double.IsInfinity(widthConstraint))
				{
					widthConstraint = sizeThatFits.Width;
				}
 
				if (double.IsInfinity(heightConstraint))
				{
					heightConstraint = sizeThatFits.Height;
				}
			}
 
			return base.GetDesiredSize(widthConstraint, heightConstraint);
		}
 
		public static void MapText(IEditorHandler handler, IEditor editor)
		{
			handler.PlatformView?.UpdateText(editor);
 
			// Any text update requires that we update any attributed string formatting
			MapFormatting(handler, editor);
		}
 
		public static void MapTextColor(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateTextColor(editor);
 
		public static void MapPlaceholder(IEditorHandler handler, IEditor editor)
		{
			handler.PlatformView?.UpdatePlaceholder(editor);
			handler.UpdateValue(nameof(IEditor.CharacterSpacing));
		}
 
		public static void MapPlaceholderColor(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdatePlaceholderColor(editor);
 
		public static void MapCharacterSpacing(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateCharacterSpacing(editor);
 
		public static void MapMaxLength(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateMaxLength(editor);
 
		public static void MapIsReadOnly(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateIsReadOnly(editor);
 
		public static void MapIsTextPredictionEnabled(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateIsTextPredictionEnabled(editor);
 
		public static void MapIsSpellCheckEnabled(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateIsSpellCheckEnabled(editor);
 
		public static void MapFont(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateFont(editor, handler.GetRequiredService<IFontManager>());
 
		public static void MapHorizontalTextAlignment(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateHorizontalTextAlignment(editor);
 
		public static void MapVerticalTextAlignment(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateVerticalTextAlignment(editor);
 
		public static void MapCursorPosition(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateCursorPosition(editor);
 
		public static void MapSelectionLength(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateSelectionLength(editor);
 
		public static void MapKeyboard(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateKeyboard(editor);
 
		public static void MapFormatting(IEditorHandler handler, IEditor editor)
		{
			handler.PlatformView?.UpdateMaxLength(editor);
 
			// Update all of the attributed text formatting properties
			handler.PlatformView?.UpdateCharacterSpacing(editor);
		}
 
		public static void MapIsEnabled(IEditorHandler handler, IEditor editor) =>
			handler.PlatformView?.UpdateIsEnabled(editor);
 
		class MauiTextViewEventProxy
		{
			bool _set;
			WeakReference<IEditor>? _virtualView;
 
			IEditor? VirtualView => _virtualView is not null && _virtualView.TryGetTarget(out var v) ? v : null;
 
			public void Connect(IEditor virtualView, MauiTextView platformView)
			{
				_virtualView = new(virtualView);
 
				platformView.ShouldChangeText += OnShouldChangeText;
				platformView.Started += OnStarted;
				platformView.Ended += OnEnded;
				platformView.TextSetOrChanged += OnTextPropertySet;
			}
 
			public void Disconnect(MauiTextView platformView)
			{
				_virtualView = null;
 
				platformView.ShouldChangeText -= OnShouldChangeText;
				platformView.Started -= OnStarted;
				platformView.Ended -= OnEnded;
				platformView.TextSetOrChanged -= OnTextPropertySet;
				if (_set)
					platformView.SelectionChanged -= OnSelectionChanged;
 
				_set = false;
			}
 
			public void SetVirtualView(MauiTextView platformView)
			{
				if (!_set)
					platformView.SelectionChanged += OnSelectionChanged;
				_set = true;
			}
 
			void OnSelectionChanged(object? sender, EventArgs e)
			{
				if (sender is MauiTextView platformView && VirtualView is IEditor virtualView)
				{
					var cursorPosition = platformView.GetCursorPosition();
					var selectedTextLength = platformView.GetSelectedTextLength();
 
					if (virtualView.CursorPosition != cursorPosition)
						virtualView.CursorPosition = cursorPosition;
 
					if (virtualView.SelectionLength != selectedTextLength)
						virtualView.SelectionLength = selectedTextLength;
				}
			}
 
			bool OnShouldChangeText(UITextView textView, NSRange range, string replacementString) =>
				VirtualView?.TextWithinMaxLength(textView.Text, range, replacementString) ?? false;
 
			void OnStarted(object? sender, EventArgs eventArgs)
			{
				if (VirtualView is IEditor virtualView)
					virtualView.IsFocused = true;
			}
 
			void OnEnded(object? sender, EventArgs eventArgs)
			{
				if (VirtualView is IEditor virtualView)
				{
					virtualView.IsFocused = false;
					virtualView.Completed();
				}
			}
 
			void OnTextPropertySet(object? sender, EventArgs e)
			{
				if (sender is MauiTextView platformView)
				{
					VirtualView?.UpdateText(platformView.Text);
				}
			}
		}
	}
}