Search code examples
xamarin.iosmaui.net-maui.shell

Maui custom iOS picker renderer conversion


I have a xamarin custom picker renderer that has the drop down arrow I am trying to convert to a handler in MAUI. I have successfully converted the Android one but I am at a loss for iOS. Any tips here?

xamarin renderer:

public class CustomPickerRenderer: PickerRenderer {
  public CustomPicker ElementV2 => Element as CustomPicker;
  public UITextFieldPadding ControlV2 => Control as UITextFieldPadding;
  private static string _defaultDownArrow = "\uf0d7";

  protected override UITextField CreateNativeControl() {
    var control = new UITextFieldPadding(RectangleF.Empty) {
      Padding = ElementV2.Padding,
      BorderStyle = UITextBorderStyle.RoundedRect,
      ClipsToBounds = true
    };

    UpdateBackground(control);

    return control;
  }

  protected void UpdateBackground(UITextField control) {
    if (control == null) return;
    control.Layer.CornerRadius = ElementV2.CornerRadius;
    control.Layer.BorderWidth = ElementV2.BorderThickness;
    control.Layer.BorderColor = ElementV2.BorderColor.ToCGColor();

    var arrowTextSymbol = !string.IsNullOrWhiteSpace(ElementV2.IconGlyph) ? ElementV2.IconGlyph: _defaultDownArrow;
    var downArrow = new UILabel {
      Text = $ "{arrowTextSymbol}   ",
      TextColor = ElementV2.IconColor.ToUIColor(),
      Font = UIFont.FromName(ElementV2.IconFontFamily, (float) ElementV2.IconSize),
      TextAlignment = UITextAlignment.Center
    };

    control.RightView = downArrow;
    control.RightViewMode = UITextFieldViewMode.Always;
  }

  protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
    if (e.PropertyName == CustomPicker.PaddingProperty.PropertyName) {
      UpdatePadding();
    }

    if (e.PropertyName == CustomPicker.BorderColorProperty.PropertyName) {
      UpdateBorderColor();
    }

    if (e.PropertyName == CustomPicker.IconGlyphProperty.PropertyName) {
      UpdateIcon();
    }

    base.OnElementPropertyChanged(sender, e);
  }

  protected void UpdateBorderColor() {
    if (Control == null) return;

    ControlV2.Layer.BorderColor = ElementV2.BorderColor.ToCGColor();
  }

  protected void UpdatePadding() {
    if (Control == null) return;

    ControlV2.Padding = ElementV2.Padding;
  }

  protected void UpdateIcon() {
    if (Control == null) return;

    var arrowTextSymbol = !string.IsNullOrWhiteSpace(ElementV2.IconGlyph) ? ElementV2.IconGlyph: _defaultDownArrow;
    var downArrow = new UILabel {
      Text = $ "{arrowTextSymbol}   ",
      TextColor = ElementV2.IconColor.ToUIColor(),
      Font = UIFont.FromName(ElementV2.IconFontFamily, (float) ElementV2.IconSize)
    };

    ControlV2.RightView = downArrow;
    ControlV2.RightViewMode = UITextFieldViewMode.Always;

    ControlV2.LeftView = new UIView(new CGRect(0, 0, 10, 0));
    ControlV2.LeftViewMode = UITextFieldViewMode.Always;
    ControlV2.Padding = ElementV2.Padding;
  }
}

Text field with padding:

public class UITextFieldPadding: UITextField {
  private Thickness _padding = new Thickness(5);

  public Thickness Padding {
    get =>_padding;
    set {
      if (_padding != value) {
        _padding = value;
        //InvalidateIntrinsicContentSize();
      }
    }
  }

  public UITextFieldPadding() {}
  public UITextFieldPadding(NSCoder coder) : base(coder) {}

  public UITextFieldPadding(CGRect rect) : base(rect) {}

  public override CGRect TextRect(CGRect forBounds) {
    var insets = new UIEdgeInsets((float) Padding.Top, (float) Padding.Left, (float) Padding.Bottom, (float) Padding.Right);
    return insets.InsetRect(forBounds);
  }

  public override CGRect PlaceholderRect(CGRect forBounds) {
    var insets = new UIEdgeInsets((float) Padding.Top, (float) Padding.Left, (float) Padding.Bottom, (float) Padding.Right);
    return insets.InsetRect(forBounds);
  }

  public override CGRect EditingRect(CGRect forBounds) {
    var insets = new UIEdgeInsets((float) Padding.Top, (float) Padding.Left, (float) Padding.Bottom, (float) Padding.Right);
    return insets.InsetRect(forBounds);
  }
}

Solution

  • This is one way to make handlers with mapping. I hope this is what you was looking for. This is not production ready code, just so you can find your way through this and make this your own.

    We start with a Picker class we can use for accessing Padding and BorderColor from our xaml-file or code behind.

    public class PickerRowEx : Picker
    {
        public static readonly BindableProperty PaddingProperty = BindableProperty.Create(nameof(Padding), typeof(Thickness), typeof(PickerRowEx),new Thickness(0));
    
        public Thickness Padding
        {
            get => (Thickness)GetValue(PaddingProperty);
            set => SetValue(PaddingProperty, value);
        }
    
        public static readonly BindableProperty BorderColorProperty = BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(PickerRowEx));
    
        public Color BorderColor
        {
            get => (Color)GetValue(BorderColorProperty);
            set => SetValue(BorderColorProperty, value);
        }
    }
    

    After this we do some multi files with partials. Starting with the standard This is the one that is common for all the other platforms and we have our mapping here, our starting point if you will. PickerRowExHandler.cs Here we see the UpdateValue that is similar to your OnElementPropertyChanged

    using Microsoft.Maui.Handlers;
    
    public partial class PickerRowExHandler
    {
        public static new PropertyMapper<IPicker, PickerHandler> Mapper = new PropertyMapper<IPicker, PickerHandler>(PickerHandler.Mapper)
        {
            ["Padding"] = MapPadding,
            ["BorderColor"] = MapBorderColor,
        };
        public PickerRowExHandler() : base(Mapper)
        {
        }
    
        public PickerRowExHandler(PropertyMapper mapper) : base(mapper)
        {
        }
    
        public override void UpdateValue(string propertyName)
        {
            base.UpdateValue(propertyName);
            if (propertyName == PickerRowEx.PaddingProperty.PropertyName)
            {
                if(VirtualView is PickerRowEx pickerRowEx)
                {
                    SetPadding(pickerRowEx.Padding);
                }
            }
    
            if (propertyName == PickerRowEx.BorderColorProperty.PropertyName)
            {
                if (VirtualView is PickerRowEx pickerRowEx)
                {
                    SetBorderColor(pickerRowEx.BorderColor);
                }
            }
        }
    
        public static void MapPadding(PickerHandler handler, IPicker picker)
        {
            if (handler is PickerRowExHandler pickerHandler && pickerHandler.VirtualView is PickerRowEx pickerRowEx)
            {
                pickerHandler.SetPadding(pickerRowEx.Padding);
            }
        }
    
        public static void MapBorderColor(PickerHandler handler, IPicker picker)
        {
            if (handler is PickerRowExHandler pickerHandler && pickerHandler.VirtualView is PickerRowEx pickerRowEx)
            {
                pickerHandler.SetBorderColor(pickerRowEx.BorderColor);
            }
        }
    }
    

    After that we do our platform specific files. PickerRowExHandler.iOS.cs. You will need to make one of those for each platform you want to use.

    using UIKit;
    
    public partial class PickerRowExHandler : PickerHandler
    {
        public void SetPadding(Thickness padding)
        {
            var margin = 5;
    
            if (PlatformView is not UITextField textField) return;
    
            textField.LeftView = new UIView(new CGRect(0, 0, padding.Left, textField.Frame.Height));
            textField.LeftViewMode = UITextFieldViewMode.Always;
    
            var fontAwesomeLabel = new UILabel
            {
                Text = "\uf0d7",
                Font = UIFont.FromName("Font Awesome 5 Free", 20),
                TextAlignment = UITextAlignment.Left,
            };
            fontAwesomeLabel.SizeToFit();
    
            var originalLabelWidth = fontAwesomeLabel.Frame.Width + margin;
            var newContainerWidth = originalLabelWidth + (float)padding.Right;
    
            var labelYPosition = (textField.Frame.Height - fontAwesomeLabel.Frame.Height) / 2;
    
            var containerView = new UIView(new CGRect(0, 0, newContainerWidth, textField.Frame.Height));
    
            fontAwesomeLabel.Frame = new CGRect((float)padding.Right, labelYPosition, originalLabelWidth, fontAwesomeLabel.Frame.Height);
    
            containerView.AddSubview(fontAwesomeLabel);
    
            textField.RightView = containerView;
            textField.RightViewMode = UITextFieldViewMode.Always;
        }
    
        public void SetBorderColor(Color color)
        {
            PlatformView.Layer.CornerRadius = 10;
            PlatformView.Layer.BorderWidth = 1;
            PlatformView.Layer.BorderColor = color.ToCGColor();
        }
    }
    

    Implementation

    Don't forget to register this control in your MauiProgram.cs before using it.

    <VerticalStackLayout
        HorizontalOptions="Center"
        Spacing="20"
        VerticalOptions="Center">
        <controls:PickerRowEx
            x:Name="MyPicker"
            Title="Pick an Item"
            Margin="10"
            Padding="0,0,0,0"
            BackgroundColor="Transparent"
            BorderColor="DarkBlue"
            HorizontalOptions="CenterAndExpand"
            HorizontalTextAlignment="Center"
            MinimumWidthRequest="200"
            SelectedIndexChanged="MyPicker_SelectedIndexChanged"
            TextColor="Blue">
            <controls:PickerRowEx.Items>
                <x:String>Green</x:String>
                <x:String>Purple</x:String>
                <x:String>Blue</x:String>
                <x:String>Yellow</x:String>
                <x:String>Magenta</x:String>
            </controls:PickerRowEx.Items>
        </controls:PickerRowEx>
    
    </VerticalStackLayout>
    

    enter image description here

    For complete code you can find it in my Repo on Github