If you are developing a mobile app, it’s worth enabling VoiceOver on iOS and considering the usability from the perspective of people who rely on these tools to access your app.
Out of the box, Xamarin Forms comes with some nice accessibility features that can control the VoiceOver experience, in particular Automation Properties allow you to annotate your XAML elements with metadata such as what text to read out when selected by VoiceOver.
In this post I won’t be focusing on these features, but instead, looking at Headings.
Headings
Consider the following screen:

Visually, these headers convey the structure of the screen to the user. However, now let’s try to navigate the page using solely VoiceOver. In this animation as we swipe left/right, the previous/next element is selected and its description is read aloud.

While all the content can be accessed, the headings just fade into the regular content if VoiceOver is your only UI.

Let’s try using the Rotor navigation control to select Headings (with a two-finger twist gesture). Now we can navigate the screen by jumping through the headers with a swipe up/down gesture, however at the moment VoiceOver just says: “No Headings Found”, which isn’t very helpful.
On iOS, elements can be given a header accessibility trait. Using this, VoiceOver considers these elements as headings, and the swipe up/down gesture will now work and jump the selection to the next/previous heading.

This now functions much better as we can quick-jump between headings and still navigate to the items around them. Now let’s build it.
How do we do it?
It turns out we don’t need much code to make this happen. We’ll create an attached property that will automatically add an effect to the element it’s added to.
We first need to define our attached property, and create the effect which will be implemented only for iOS:
using SystemSystem;using SystemSystem.LinqLinq;using Xamarin.Forms;
namespace XamAppWithHeadersXamAppWithHeaders{ public static class AccessibilityAccessibility { [Accessibility.AccessibilityTraitFlagsFlagsAttribute.FlagsAttribute()Initializes a new instance of the FlagsAttribute class.]Accessibility.AccessibilityTrait public enum AccessibilityTraitAccessibility.AccessibilityTrait { NoneAccessibility.AccessibilityTrait.None, HeaderAccessibility.AccessibilityTrait.Header }
public static readonly BindablePropertyAccessibility AccessibilityTraitsProperty = BindableProperty.CreateAttached("AccessibilityTraits", typeof(AccessibilityTraitAccessibility.AccessibilityTrait), typeof(AccessibilityAccessibility), AccessibilityTraitAccessibility.AccessibilityTrait.NoneAccessibility.AccessibilityTrait.None, propertyChanged: OnAccessibilityTraitsChangedvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue));
public static AccessibilityTraitAccessibility.AccessibilityTrait GetAccessibilityTraitsAccessibility.AccessibilityTrait Accessibility.GetAccessibilityTraits(BindableObject view)(BindableObject view) { return (AccessibilityTraitAccessibility.AccessibilityTrait)view.GetValueAccessibility.AccessibilityTrait Accessibility.GetAccessibilityTraits(BindableObject view)(AccessibilityTraitsProperty); }
public static voidvoidSpecifies a return value type for a method that does not return a value. SetAccessibilityTraitsvoid Accessibility.SetAccessibilityTraits(BindableObject view, Accessibility.AccessibilityTrait value)(BindableObject view, AccessibilityTraitAccessibility.AccessibilityTrait valueAccessibility.AccessibilityTrait value) { view.SetValuevoid Accessibility.SetAccessibilityTraits(BindableObject view, Accessibility.AccessibilityTrait value)(AccessibilityTraitsProperty, valueAccessibility.AccessibilityTrait value); }
static voidvoidSpecifies a return value type for a method that does not return a value. OnAccessibilityTraitsChangedvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)(BindableObject bindable, objectobjectSupports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy. oldValueobject oldValue, objectobjectSupports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy. newValueobject newValue) { if (!void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)(bindable is Viewvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue) viewvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue))) { return; }
varAccessibility.AccessibilityTrait newTraitsAccessibility.AccessibilityTrait newTraits = (AccessibilityTraitAccessibility.AccessibilityTrait)newValueobject newValue; varboolRepresents a Boolean ( or ) value. hasTraitsbool hasTraits = newTraitsAccessibility.AccessibilityTrait newTraits !=bool hasTraits AccessibilityTraitAccessibility.AccessibilityTrait.NoneAccessibility.AccessibilityTrait.None;
if (hasTraitsbool hasTraits) { if (!void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)view.Effectsvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue).OfTypevoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)<void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)AccessibilityTraitEffectAccessibility.AccessibilityTraitEffect>void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)().Anyvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)()) { view.Effectsvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue).Addvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)(new AccessibilityTraitEffectAccessibility.AccessibilityTraitEffect()); } } else { varvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue) accessibilityTrait = view.Effects.OfType<AccessibilityTraitEffectAccessibility.AccessibilityTraitEffect>().FirstOrDefault(); if (accessibilityTrait !=void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue) null) { view.Effectsvoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue).Removevoid Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)(accessibilityTrait); } } }
public class AccessibilityTraitEffectAccessibility.AccessibilityTraitEffect :Accessibility.AccessibilityTraitEffect RoutingEffectAccessibility.AccessibilityTraitEffect { public AccessibilityTraitEffectAccessibility.AccessibilityTraitEffect.AccessibilityTraitEffect()() :Accessibility.AccessibilityTraitEffect.AccessibilityTraitEffect() base("XamAppWithHeaders.AccessibilityTraitEffect") { } } }}Be sure to change XamAppWithHeaders in the RoutingEffect base call to match your own namespace.
Now in our iOS project we will implement the effect and actually set the accessibility trait:
using UIKit;using Xamarin.Forms;using Xamarin.Forms.Platform.iOS;using static XamAppWithHeadersXamAppWithHeaders.Accessibility;
[assembly: ResolutionGroupName("XamAppWithHeaders")][assembly: ExportEffect(typeof(XamAppWithHeadersXamAppWithHeaders.iOSiOS.AccessibilityTraitEffectAccessibilityTraitEffect), "AccessibilityTraitEffect")]
namespace XamAppWithHeadersXamAppWithHeaders.iOSiOS{ public class AccessibilityTraitEffectAccessibilityTraitEffect :AccessibilityTraitEffect PlatformEffectAccessibilityTraitEffect { protected override voidvoidSpecifies a return value type for a method that does not return a value. OnAttachedvoid AccessibilityTraitEffect.OnAttached()() { AddAccessibilityTraitsvoid AccessibilityTraitEffect.AddAccessibilityTraits()(); }
protected override voidvoidSpecifies a return value type for a method that does not return a value. OnDetachedvoid AccessibilityTraitEffect.OnDetached()() { }
protected override voidvoidSpecifies a return value type for a method that does not return a value. OnElementPropertyChangedvoid AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)(SystemSystem.ComponentModelComponentModel.PropertyChangedEventArgsPropertyChangedEventArgsProvides data for the PropertyChanged event. argsPropertyChangedEventArgs args) { if (argsPropertyChangedEventArgs args.PropertyNamestring? PropertyChangedEventArgs.PropertyNameGets the name of the property that changed.ReturnsThe name of the property that changed. ==void AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args) AccessibilityTraitsPropertyvoid AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args).PropertyNamevoid AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)) { AddAccessibilityTraitsvoid AccessibilityTraitEffect.AddAccessibilityTraits()(); } else { basevoid AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args).OnElementPropertyChangedvoid AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)(argsPropertyChangedEventArgs args); } }
voidvoidSpecifies a return value type for a method that does not return a value. AddAccessibilityTraitsvoid AccessibilityTraitEffect.AddAccessibilityTraits()() { varvoid AccessibilityTraitEffect.AddAccessibilityTraits() traits = Control.AccessibilityTraits;
varvoid AccessibilityTraitEffect.AddAccessibilityTraits() newTraits = GetAccessibilityTraits(Element);
if ((newTraits &void AccessibilityTraitEffect.AddAccessibilityTraits() AccessibilityTraitvoid AccessibilityTraitEffect.AddAccessibilityTraits().Headervoid AccessibilityTraitEffect.AddAccessibilityTraits()) >void AccessibilityTraitEffect.AddAccessibilityTraits() 0) traits |=void AccessibilityTraitEffect.AddAccessibilityTraits() UIAccessibilityTraitvoid AccessibilityTraitEffect.AddAccessibilityTraits().Headervoid AccessibilityTraitEffect.AddAccessibilityTraits();
Controlvoid AccessibilityTraitEffect.AddAccessibilityTraits().AccessibilityTraitsvoid AccessibilityTraitEffect.AddAccessibilityTraits() = traits; } }}… and that’s it, we can now bring the namespace into our xaml view and add it to any elements that we want to indicate as a heading.
<Label local:Accessibility.AccessibilityTraits="Header" Text="First Header" Style="{StaticResource headerStyle}" />However, you can see that we already have a shared style for heading as is often the case with a real world app, in this case headerStyle. It turns out that you can add attached properties via a shared style, which looks like this:
<Style x:Key="headerStyle" TargetType="Label"> <Setter Property="FontAttributes" Value="Bold" /> <Setter Property="FontSize" Value="22" /> <Setter Property="local:Accessibility.AccessibilityTraits" Value="Header" /></Style>Closing Thoughts
With just 2 code files and 1 line added to your heading styles, it couldn’t be much simpler to make your app more accessible for people who rely on VoiceOver. Give it a try 😀
A working sample can be found here:
Comments