0
votes

I'm trying to implement an Editor with hint text functionality for a Xamarin.Forms project. This is trivial in Android, because the underlying EntryEditText control has a Hint property. In iOS, the implementation is a bit more complex because the UITextView class does not implement hint text.

I don't like the technique, "set text to the placeholder, clear it if typing starts, return it if typing ends and the text is blank". It means I have to do extra work to tell if the control's blank, and there's a lot of fiddling with the text color involved. But I've been having so much trouble I'm going to have to resort to it. Maybe someone can help me with this.

I started with the answer to Placeholder in UITextView. I started a new Xamarin iOS project and stumbled through a rough Obj-C to C# conversion, and it worked great with a minor change: the Font property of the UITextView isn't initialized yet in the constructor, so I had to override AwakeFromNib() to set the placeholder label's font. I tested it and it worked, so I brought that file into a Xamarin Forms project, and things started getting a little nutty.

The first problem is it turns out apparently MonoTouch has some slight API differences in Xamarin Forms, such as using some types like RectangleF instead of CGRect. This was obvious, if not unexpected. I've been wrestling with some other differences for the past few days, and can't seem to overcome them in a way that makes me happy. Here's my file, trimmed down significantly because I've been trying all kinds of debugging things:

using System;
using MonoTouch.UIKit;
using MonoTouch.Foundation;
using MonoTouch.CoreGraphics;
using System.Drawing;

namespace TestCustomRenderer.iOS {
    public class PlaceholderTextView : UITextView {

        private UILabel _placeholderLabel;
        private NSObject _notificationToken;

        private const double UI_PLACEHOLDER_TEXT_CHANGED_ANIMATION_DURATION = 0.25;

        private string _placeholder;
        public string Placeholder {
            get {
                return _placeholder;
            }
            set {
                _placeholder = value;
                if (_placeholderLabel != null) {
                    _placeholderLabel.Text = _placeholder;
                }
            }
        }

        public PlaceholderTextView() : base(RectangleF.Empty) {
            Initialize();
        }

        private void Initialize() {
            _notificationToken = NSNotificationCenter.DefaultCenter.AddObserver(TextDidChangeNotification, HandleTextChanged);
            _placeholderLabel = new UILabel(new RectangleF(8, 8, this.Bounds.Size.Width - 16, 25)) {
                LineBreakMode = UILineBreakMode.WordWrap,
                Lines = 1,
                BackgroundColor = UIColor.Green,
                TextColor = UIColor.Gray,
                Alpha = 1.0f,
                Text = Placeholder
            };

            AddSubview(_placeholderLabel);
            _placeholderLabel.SizeToFit();
            SendSubviewToBack(_placeholderLabel);
        }

        public override void DrawRect(RectangleF area, UIViewPrintFormatter formatter) {
            base.DrawRect(area, formatter);

            if (Text.Length == 0 && Placeholder.Length > 0) {
                _placeholderLabel.Alpha = 1;
            }
        }

        private void HandleTextChanged(NSNotification notification) {
            if (Placeholder.Length == 0) {
                return;
            }

            UIView.Animate(UI_PLACEHOLDER_TEXT_CHANGED_ANIMATION_DURATION, () => {
                if (Text.Length == 0) {
                    _placeholderLabel.Alpha = 1;
                } else {
                    _placeholderLabel.Alpha = 0;
                }
            });
        } 

        public override void AwakeFromNib() {
            base.AwakeFromNib();

            _placeholderLabel.Font = this.Font;
        }

        protected override void Dispose(bool disposing) {
            base.Dispose(disposing);

            if (disposing) {
                NSNotificationCenter.DefaultCenter.RemoveObserver(_notificationToken);
                _placeholderLabel.Dispose();
            }
        }

    }
}

A notable change here is relocation of the label's initialization from DrawRect() to the constructor. As far as I can tell, Xamarin never lets DrawRect() be called. You'll also note I'm not setting the Font property. It turned out in the iOS MonoTouch project, sometimes the parent's font was null and it's illegal to set the label's font to null as well. It seems at some point after construction Xamarin sets the font, so it's safe to set that property in AwakeFromNib().

I wrote a quick Editor-derived class and a custom renderer so Xamarin Forms could render the control, the Renderer is slightly of note because I derived from NativeRenderer instead of EditorRenderer. I needed to call SetNativeControl() from an overridden OnModelSet(), but peeking at the assembly viewer showed that EditorRenderer makes some private calls I'll have to re-implement in mine. Boo. Not posted because this is already huge, but I can edit it in if needed.

The code above is notable because the placeholder isn't visible at all. It looks like in iOS-oriented MonoTouch, you typically initialize a control with a frame, and resizing is a rare enough circumstance you can assume it doesn't happen. In Xamarin Forms, layout is performed by layout containers, so a constructor-provided frame is irrelevant. However, the size of the label is intended to be set in the constructor, so it ends up having negative width. Whoops.

I assumed this could be solved by moving instantiation of the label into AwakeFromNib(), or at least sizing it there. This is when I discovered that for some reason, AwakeFromNib() isn't called in the control. Welp. I tried to find an equivalent callback/event that happened late enough for the bounds to be set, but couldn't find anything on the iOS side. After trying many, many things, I noticed the custom renderer received property change events for the Xamarin Forms Model side of this mess. So, if I listen for Height/Width change events, I can then call a method on the label to give it a reasonable size based on the current control. That exposed another problem.

I cannot find a way to set the label's font to match the UITextView's font. In the constructor, the Font property is null. This is true in both the iOS and Xamarin Forms project. In the iOS project, by the time AwakeFromNib() is called, the property is initialized and all is well. In the XF project, it's never called, and even when I pull stunts like invoking a method from a 5-second delayed Task (to ensure the control is displayed), the property remains null.

Logic and iOS documentation dictates the default value for the font should be 17-point Helvetica. This is true for the placeholder label if I fudge the size so it's visible. It is not true for the UITextView control, though since it reports its font as null I'm unable to see what the font actually is. If I manually set it all is well, of course, but I'd like to be able to handle the default case. This seems like a bug; the box seems to be lying about its font. I have a feeling it's related to whatever reason the Xamarin.Forms.Editor class doesn't have a Font property.

So I'm looking for the answer to two questions:

  1. If I'm extending an iOS control in XF to add a subview, what is the best way to handle sizing that subview? I've found Height/Width changes raise events in the renderer, is this the only available way?
  2. When the property has not been set by a user, is the Font of a UITextView in Xamarin Forms ever set to a non-null value? I can live with a requirement that this control requires the font to be explicitly set, but it's yucky and I'd like to avoid it.

I'm hoping I've missed something obvious because I started barking up the wrong trees.

1

1 Answers

0
votes

If I'm extending an iOS control in XF to add a subview, what is the best way to handle sizing that subview? I've found Height/Width changes raise events in the renderer, is this the only available way?

This is the only way I know of since the exposed elements of the renderer are so limited.

When the property has not been set by a user, is the Font of a UITextView in Xamarin Forms ever set to a non-null value? I can live with a requirement that this control requires the font to be explicitly set, but it's yucky and I'd like to avoid it.

No, the Font is not assigned a default non-null value.