UIAppearance and custom properties in Xamarin.iOS

UIAppearance is a fairly known and used class on iOS. It lets you configure the default appearance for controls, even custom ones.

Its use is trivial, just assign any property like you would do on the “real” control, like this:

UITextfield.appearance.backgroundColor = UIColor.grayColor;

But what if we have more complex, custom properties?

Objective-C has as an interesting feature called “categories” that basically lets you extend classes outside of your control adding methods (and properties), implementing protocols etc. Swift of course has extensions, but for the purposes of this post we’re going to focus on Objective-C categories.

If you wanted to add a placeholderTextColor property on a UIText field, you just need to define a category like this:

@interface UITextField (Placeholder)

@property UIColor* placeholderTextColor;

@end

@implementation UITextField (Placeholder)

-(UIColor*)placeholderTextColor {
    // omitted for brevity
}

-(void)setPlaceholderTextColor:(UIColor*)placeholderTextColor {
    // omitted for brevity
}

@end

and then use it as any other property:

UITextField someField = ...;
someField.placeholderTextColor = UIColor.grayColor;

Objective-C even lets you use it in UIAppearance:

UITextField.appearance.placeholderTextColor = UIColor.purpleColor;

If all this looks familiar, it’s probably because C# has a similar feature called Extension Methods that has a similar purpose, albeit a bit limited compared to the Objective-C counterpart (you can just add methods).

Extending native controls with Extension Methods.

Our PlaceholderTextColor sample above would be written like this:

public static class UITextFieldPlaceholderTextColorExtensions
{
    public static UIColor PlaceholderTextColor(this UITextField self)
    {
        var attributeName = UIStringAttributeKey.ForegroundColor;
        var color = self.AttributedPlaceholder.GetAttribute(attributeName, 0, out _);
        return (UIColor) color;
    }

    public static void SetPlaceholderTextColor(this UITextField self, UIColor placeholderTextColor)
    {
        if (self.AttributedPlaceholder is null)
        {
            if (self.Placeholder is null)
            {
                // Neither Placeholder nor AttributedPlaceholder are set
                // nothing to do
                return;
            }

            // We've got a plain Placeholder, apply the right attribute and set
            // it as AttributedPlaceHolder
            self.AttributedPlaceholder = new NSAttributedString(self.Placeholder,
                new NSDictionary(UIStringAttributeKey.ForegroundColor, placeholderTextColor));
        }
        else // An AttributedPlaceholder is already set
        {
            var attributedString = new NSMutableAttributedString(self.AttributedPlaceholder);

            // When passed a null UIColor we remove the attribute
            if (placeholderTextColor is null)
            {
                attributedString.RemoveAttribute(UIStringAttributeKey.ForegroundColor,
                    new NSRange(0, self.AttributedPlaceholder.Length));
            }
            else
            {
                attributedString.SetAttributes(
                    new NSDictionary(UIStringAttributeKey.ForegroundColor, placeholderTextColor),
                    new NSRange(0, self.AttributedPlaceholder.Length));
            }

            self.AttributedPlaceholder = attributedString;
        }
    }
}

This lets us set a color for the Placeholder text like this:

var someField = ...;
someField.SetPlaceHolderTextColor(UIColor.Gray);

Sorry, no nice property syntax with extension methods (yet).

Sadly Extension Methods are not (automatically) exposed to Objective-C, and of course the Xamarin bindings for the UIAppearance-derived classes have no idea about our extension methods!

But yet, we still want to fully take advantage to UIAppearance, after all one of the biggest reasons to use Xamarin is that it gives you full access to all the native features!

Enter the CategoryAttribute.

Exporting extension methods as categories

CategoryAttribute lets you export a static class containing extension methods as a category for an Objective-C class to its runtime, basically letting you write categories using C#. All we need to do is decorate the class with the attribute, passing the type of the class we want to extend:

[Category(typeof(UITextField))]
public static class UITextFieldPlaceholderTextColorExtensions
{
    ...
}

Please note that if you use the category only from Objective-C world and never from managed C# code, it may get removed by the linker! One way to avoid it is to apply the PreserveAttribute to the class. It’s not our case, but it’s something to keep in mind.

Exporting the category is just part of the story though. We also need to export the two extension methods using the usual ExportAttribute:

[Category(typeof(UITextField))]
public static class UITextFieldPlaceholderTextColorExtensions
{
    [Export("placeholderTextColor")]
    public static UIColor PlaceholderTextColor(this UITextField self)
    {
        ...
    }

    [Export("setPlaceholderTextColor:")]
    public static void SetPlaceholderTextColor(this UITextField self, UIColor placeholderTextColor)
    {
        ...
    }
}

Note the colon at the end of the setter’s selector, it means the method accepts one single parameter (and it’s required).


This won’t change anything from the C# on-the-spot usage point of view, but makes it possible for UIAppearance to set a default for that property. The problem is, as we said earlier, UIAppearance bindings have no idea our extension/category exists, so this code simply won’t compile:

UITextField.Appearance.SetPlaceholderTextColor(UIColor.Brown);

Wiring our extension to UIAppearance

Objective-C is a message-passing-based environment. It’s quite dynamic and classes can dynamically support any property, not unlike C#’s Reflection APIs. The way Objective-C does it is via “selectors”, which are objects that describe a message (roughly equivalent to .NET’s MethodInfo class).

var selector = new Selector("setPlaceholderTextColor:");
UITextField.Appearance.PerformSelector(selector, UIColor.Brown);

Note the parameter to the Selector constructor is the same as the one passed in the setter’s Export attribute.

We can use the same approach to call the getter:

var selector = new Selector("placeholderTextColor");
var color = UITextField.Appearance.PerformSelector(selector) as UIColor;

Of course having to type a literal string everythime is less than ideal. One way to encapsulate everything nicely is to write two extension methods for UITextFieldAppearance:

private static readonly Selector PlaceholderTextColorSetter = new Selector("setPlaceholderTextColor:");

public static void SetPlaceholderTextColor(this UITextField.UITextFieldAppearance self,
    UIColor placeholderTextColor)
{
    self.PerformSelector(PlaceholderTextColorSetter, placeholderTextColor);
}

Putting it all together

In this Gist you can find the complete code for the category and the extensions, hope you’ll find it useful!


Fabio Di Peri

833 Words

2020-04-01 19:00 +0000