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.

Rotor control

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 System
System
;
using System
System
.Linq
Linq
;
using Xamarin.Forms;
namespace XamAppWithHeaders
XamAppWithHeaders
{
public static class Accessibility
Accessibility
{
[
Accessibility.AccessibilityTrait
Flags
FlagsAttribute.FlagsAttribute()
Initializes a new instance of the FlagsAttribute class.
]
Accessibility.AccessibilityTrait
public enum AccessibilityTrait
Accessibility.AccessibilityTrait
{
None
Accessibility.AccessibilityTrait.None
,
Header
Accessibility.AccessibilityTrait.Header
}
public static readonly BindableProperty
Accessibility
AccessibilityTraitsProperty =
BindableProperty.CreateAttached("AccessibilityTraits", typeof(AccessibilityTrait
Accessibility.AccessibilityTrait
), typeof(Accessibility
Accessibility
), AccessibilityTrait
Accessibility.AccessibilityTrait
.None
Accessibility.AccessibilityTrait.None
, propertyChanged: OnAccessibilityTraitsChanged
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
);
public static AccessibilityTrait
Accessibility.AccessibilityTrait
GetAccessibilityTraits
Accessibility.AccessibilityTrait Accessibility.GetAccessibilityTraits(BindableObject view)
(BindableObject view)
{
return (AccessibilityTrait
Accessibility.AccessibilityTrait
)view.GetValue
Accessibility.AccessibilityTrait Accessibility.GetAccessibilityTraits(BindableObject view)
(AccessibilityTraitsProperty);
}
public static void
void
Specifies a return value type for a method that does not return a value.
SetAccessibilityTraits
void Accessibility.SetAccessibilityTraits(BindableObject view, Accessibility.AccessibilityTrait value)
(BindableObject view, AccessibilityTrait
Accessibility.AccessibilityTrait
value
Accessibility.AccessibilityTrait value
)
{
view.SetValue
void Accessibility.SetAccessibilityTraits(BindableObject view, Accessibility.AccessibilityTrait value)
(AccessibilityTraitsProperty, value
Accessibility.AccessibilityTrait value
);
}
static void
void
Specifies a return value type for a method that does not return a value.
OnAccessibilityTraitsChanged
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
(BindableObject bindable, object
object
Supports 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.
oldValue
object oldValue
, object
object
Supports 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.
newValue
object newValue
)
{
if (!
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
(bindable is View
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
view
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
))
{
return;
}
var
Accessibility.AccessibilityTrait
newTraits
Accessibility.AccessibilityTrait newTraits
= (AccessibilityTrait
Accessibility.AccessibilityTrait
)newValue
object newValue
;
var
bool
Represents a Boolean ( or ) value.
hasTraits
bool hasTraits
= newTraits
Accessibility.AccessibilityTrait newTraits
!=
bool hasTraits
AccessibilityTrait
Accessibility.AccessibilityTrait
.None
Accessibility.AccessibilityTrait.None
;
if (hasTraits
bool hasTraits
)
{
if (!
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
view.Effects
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
.OfType
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
<
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
AccessibilityTraitEffect
Accessibility.AccessibilityTraitEffect
>
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
().Any
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
())
{
view.Effects
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
.Add
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
(new AccessibilityTraitEffect
Accessibility.AccessibilityTraitEffect
());
}
}
else
{
var
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
accessibilityTrait = view.Effects.OfType<AccessibilityTraitEffect
Accessibility.AccessibilityTraitEffect
>().FirstOrDefault();
if (accessibilityTrait !=
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
null)
{
view.Effects
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
.Remove
void Accessibility.OnAccessibilityTraitsChanged(BindableObject bindable, object oldValue, object newValue)
(accessibilityTrait);
}
}
}
public class AccessibilityTraitEffect
Accessibility.AccessibilityTraitEffect
:
Accessibility.AccessibilityTraitEffect
RoutingEffect
Accessibility.AccessibilityTraitEffect
{
public AccessibilityTraitEffect
Accessibility.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 XamAppWithHeaders
XamAppWithHeaders
.Accessibility;
[assembly: ResolutionGroupName("XamAppWithHeaders")]
[assembly: ExportEffect(typeof(XamAppWithHeaders
XamAppWithHeaders
.iOS
iOS
.AccessibilityTraitEffect
AccessibilityTraitEffect
), "AccessibilityTraitEffect")]
namespace XamAppWithHeaders
XamAppWithHeaders
.iOS
iOS
{
public class AccessibilityTraitEffect
AccessibilityTraitEffect
:
AccessibilityTraitEffect
PlatformEffect
AccessibilityTraitEffect
{
protected override void
void
Specifies a return value type for a method that does not return a value.
OnAttached
void AccessibilityTraitEffect.OnAttached()
()
{
AddAccessibilityTraits
void AccessibilityTraitEffect.AddAccessibilityTraits()
();
}
protected override void
void
Specifies a return value type for a method that does not return a value.
OnDetached
void AccessibilityTraitEffect.OnDetached()
()
{
}
protected override void
void
Specifies a return value type for a method that does not return a value.
OnElementPropertyChanged
void AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)
(System
System
.ComponentModel
ComponentModel
.PropertyChangedEventArgs
PropertyChangedEventArgs
Provides data for the PropertyChanged event.
args
PropertyChangedEventArgs args
)
{
if (args
PropertyChangedEventArgs args
.PropertyName
string? PropertyChangedEventArgs.PropertyName
Gets the name of the property that changed.
The name of the property that changed.
==
void AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)
AccessibilityTraitsProperty
void AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)
.PropertyName
void AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)
)
{
AddAccessibilityTraits
void AccessibilityTraitEffect.AddAccessibilityTraits()
();
}
else
{
base
void AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)
.OnElementPropertyChanged
void AccessibilityTraitEffect.OnElementPropertyChanged(PropertyChangedEventArgs args)
(args
PropertyChangedEventArgs args
);
}
}
void
void
Specifies a return value type for a method that does not return a value.
AddAccessibilityTraits
void AccessibilityTraitEffect.AddAccessibilityTraits()
()
{
var
void AccessibilityTraitEffect.AddAccessibilityTraits()
traits = Control.AccessibilityTraits;
var
void AccessibilityTraitEffect.AddAccessibilityTraits()
newTraits = GetAccessibilityTraits(Element);
if ((newTraits &
void AccessibilityTraitEffect.AddAccessibilityTraits()
AccessibilityTrait
void AccessibilityTraitEffect.AddAccessibilityTraits()
.Header
void AccessibilityTraitEffect.AddAccessibilityTraits()
) >
void AccessibilityTraitEffect.AddAccessibilityTraits()
0) traits |=
void AccessibilityTraitEffect.AddAccessibilityTraits()
UIAccessibilityTrait
void AccessibilityTraitEffect.AddAccessibilityTraits()
.Header
void AccessibilityTraitEffect.AddAccessibilityTraits()
;
Control
void AccessibilityTraitEffect.AddAccessibilityTraits()
.AccessibilityTraits
void 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:

slang25/xamarin-forms-accessiblity-traits-demo
A demo XF iOS app showing how to use header accessibility traits - slang25/xamarin-forms-accessiblity-traits-demo