Skip to content

Commit 390852d

Browse files
authored
Merge pull request #580 from dotnet/dev/TJ/iOSChanges
[DeveloperBalance] Many iOS Accessibility Changes
2 parents 714656a + c7e8663 commit 390852d

File tree

11 files changed

+148
-15
lines changed

11 files changed

+148
-15
lines changed
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
1-
namespace DeveloperBalance;
1+
using DeveloperBalance.Services;
2+
3+
namespace DeveloperBalance;
24

35
public partial class App : Application
46
{
7+
public static IServiceProvider? ServiceProvider { get; private set; }
8+
59
public App()
610
{
711
InitializeComponent();
12+
13+
var serviceCollection = new ServiceCollection();
14+
ConfigureServices(serviceCollection);
15+
ServiceProvider = serviceCollection.BuildServiceProvider();
816
}
917

1018
protected override Window CreateWindow(IActivationState? activationState)
1119
{
1220
return new Window(new AppShell());
1321
}
22+
23+
void ConfigureServices(IServiceCollection services)
24+
{
25+
#if IOS
26+
services.AddSingleton<IAsyncAnnouncement, SemanticScreenReaderAsyncImplementation>();
27+
#endif
28+
}
1429
}

9.0/Apps/DeveloperBalance/PageModels/ManageMetaPageModel.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ private async Task SaveCategories()
4747
}
4848

4949
await AppShell.DisplayToastAsync("Categories saved");
50+
await AnnouncementHelper.Announce("Categories saved");
5051
}
5152

5253
[RelayCommand]
@@ -55,6 +56,7 @@ private async Task DeleteCategory(Category category)
5556
Categories.Remove(category);
5657
await _categoryRepository.DeleteItemAsync(category);
5758
await AppShell.DisplayToastAsync("Category deleted");
59+
await AnnouncementHelper.Announce("Category deleted");
5860
}
5961

6062
[RelayCommand]
@@ -64,6 +66,7 @@ private async Task AddCategory()
6466
Categories.Add(category);
6567
await _categoryRepository.SaveItemAsync(category);
6668
await AppShell.DisplayToastAsync("Category added");
69+
await AnnouncementHelper.Announce("Category added");
6770
}
6871

6972
[RelayCommand]
@@ -75,6 +78,7 @@ private async Task SaveTags()
7578
}
7679

7780
await AppShell.DisplayToastAsync("Tags saved");
81+
await AnnouncementHelper.Announce("Tags saved");
7882
}
7983

8084
[RelayCommand]
@@ -83,6 +87,7 @@ private async Task DeleteTag(Tag tag)
8387
Tags.Remove(tag);
8488
await _tagRepository.DeleteItemAsync(tag);
8589
await AppShell.DisplayToastAsync("Tag deleted");
90+
await AnnouncementHelper.Announce("Tags deleted");
8691
}
8792

8893
[RelayCommand]
@@ -92,6 +97,7 @@ private async Task AddTag()
9297
Tags.Add(tag);
9398
await _tagRepository.SaveItemAsync(tag);
9499
await AppShell.DisplayToastAsync("Tag added");
100+
await AnnouncementHelper.Announce("Tags added");
95101
}
96102

97103
[RelayCommand]

9.0/Apps/DeveloperBalance/PageModels/ProjectDetailPageModel.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ private async Task TaskCompleted(ProjectTask task)
156156
OnPropertyChanged(nameof(HasCompletedTasks));
157157
}
158158

159-
160159
[RelayCommand]
161160
private async Task Save()
162161
{
@@ -245,14 +244,26 @@ private async Task ToggleTag(Tag tag)
245244
if (tag.IsSelected)
246245
{
247246
await _tagRepository.SaveItemAsync(tag, _project.ID);
247+
AllTags = new(AllTags);
248+
await AnnouncementHelper.Announce($"{tag.Title} selected");
248249
}
249250
else
250251
{
251252
await _tagRepository.DeleteItemAsync(tag, _project.ID);
253+
AllTags = new(AllTags);
254+
await AnnouncementHelper.Announce($"{tag.Title} unselected");
252255
}
253256
}
257+
else
258+
{
259+
AllTags = new(AllTags);
260+
}
261+
}
254262

255-
AllTags = new(AllTags);
263+
[RelayCommand]
264+
private async Task IconSelected(IconData icon)
265+
{
266+
await AnnouncementHelper.Announce($"{icon.Description} selected");
256267
}
257268

258269
[RelayCommand]

9.0/Apps/DeveloperBalance/Pages/Controls/TaskView.xaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
x:Class="DeveloperBalance.Pages.Controls.TaskView"
1010
StrokeShape="RoundRectangle 20"
1111
Background="{AppThemeBinding Light={StaticResource LightSecondaryBackground}, Dark={StaticResource DarkSecondaryBackground}}"
12+
SemanticProperties.Description="{Binding Title, StringFormat='{0} Task'}"
1213
x:DataType="models:ProjectTask">
1314

1415
<effectsView:SfEffectsView
@@ -51,7 +52,7 @@
5152
<Label Grid.Column="1"
5253
Text="{Binding Title}"
5354
VerticalOptions="Center"
54-
LineBreakMode="TailTruncation"
55+
LineBreakMode="WordWrap"
5556
AutomationProperties.IsInAccessibleTree="False" />
5657
</Grid>
5758
</shimmer:SfShimmer.Content>

9.0/Apps/DeveloperBalance/Pages/MainPage.xaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
<pullToRefresh:SfPullToRefresh.PullableContent>
3636
<ScrollView>
3737
<VerticalStackLayout Spacing="{StaticResource LayoutSpacing}" Padding="{StaticResource LayoutPadding}">
38-
<Label Text="Task Categories" Style="{StaticResource Title2}"/>
38+
<Label Text="Task Categories" Style="{StaticResource Title2}" SemanticProperties.HeadingLevel="Level1"/>
3939
<controls:CategoryChart />
40-
<Label Text="Projects" Style="{StaticResource Title2}"/>
40+
<Label Text="Projects" Style="{StaticResource Title2}" SemanticProperties.HeadingLevel="Level1"/>
4141
<ScrollView Orientation="Horizontal" Margin="-30,0">
4242
<HorizontalStackLayout
4343
Spacing="15" Padding="30,0"
@@ -53,8 +53,8 @@
5353
</BindableLayout.ItemTemplate>
5454
</HorizontalStackLayout>
5555
</ScrollView>
56-
<Grid HeightRequest="44">
57-
<Label Text="Tasks" Style="{StaticResource Title2}" VerticalOptions="Center"/>
56+
<Grid MinimumHeightRequest="44">
57+
<Label Text="Tasks" Style="{StaticResource Title2}" VerticalOptions="Center" SemanticProperties.HeadingLevel="Level1"/>
5858
<ImageButton
5959
Source="{StaticResource IconClean}"
6060
HorizontalOptions="End"

9.0/Apps/DeveloperBalance/Pages/ManageMetaPage.xaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,12 @@
8383

8484
<Grid ColumnSpacing="{StaticResource LayoutSpacing}" ColumnDefinitions="*,Auto" Margin="0,10">
8585
<Button Text="Save" Command="{Binding SaveCategoriesCommand}"
86+
SemanticProperties.Description="Save Categories"
8687
HeightRequest="{OnIdiom 44,Desktop=60}" Grid.Column="0" />
8788

8889
<Button ImageSource="{StaticResource IconAdd}"
8990
Command="{Binding AddCategoryCommand}" Grid.Column="1"
90-
SemanticProperties.Description="Add" />
91+
SemanticProperties.Description="Add Categories" />
9192
</Grid>
9293

9394
<Label Text="Tags" Style="{StaticResource Title2}" />
@@ -125,11 +126,12 @@
125126

126127
<Grid ColumnSpacing="{StaticResource LayoutSpacing}" ColumnDefinitions="*,Auto" Margin="0,10">
127128
<Button Text="Save" Command="{Binding SaveTagsCommand}"
129+
SemanticProperties.Description="Save Tags"
128130
HeightRequest="{OnIdiom 44,Desktop=60}" Grid.Column="0" />
129131

130132
<Button ImageSource="{StaticResource IconAdd}"
131133
Command="{Binding AddTagCommand}" Grid.Column="1"
132-
SemanticProperties.Description="Add" />
134+
SemanticProperties.Description="Add Tags" />
133135
</Grid>
134136
</VerticalStackLayout>
135137
</ScrollView>

9.0/Apps/DeveloperBalance/Pages/ProjectDetailPage.xaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,21 +69,22 @@
6969
<sf:SfTextInputLayout
7070
Hint="Name" >
7171
<Entry
72-
Text="{Binding Name}" />
72+
Text="{Binding Name}" SemanticProperties.Description="Name" />
7373
</sf:SfTextInputLayout>
7474

7575
<sf:SfTextInputLayout
7676
Hint="Description">
7777
<Entry
78-
Text="{Binding Description}" />
78+
Text="{Binding Description}" SemanticProperties.Description="Description" />
7979
</sf:SfTextInputLayout>
8080

8181
<sf:SfTextInputLayout
8282
Hint="Category">
8383
<Picker
8484
ItemsSource="{Binding Categories}"
8585
SelectedItem="{Binding Category}"
86-
SelectedIndex="{Binding CategoryIndex}" />
86+
SelectedIndex="{Binding CategoryIndex}"
87+
SemanticProperties.Description="Category" />
8788
</sf:SfTextInputLayout>
8889

8990
<Label
@@ -94,6 +95,8 @@
9495
SelectionMode="Single"
9596

9697
SelectedItem="{Binding Icon}"
98+
SelectionChangedCommand="{Binding IconSelectedCommand}"
99+
SelectionChangedCommandParameter="{Binding Icon}"
97100
ItemsSource="{Binding Icons}">
98101
<CollectionView.ItemTemplate>
99102
<DataTemplate x:DataType="models:IconData">

9.0/Apps/DeveloperBalance/Pages/TaskDetailPage.xaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
<sf:SfTextInputLayout Hint="Task">
2525
<Entry
2626
Text="{Binding Title}"
27-
SemanticProperties.Description="Title" />
27+
SemanticProperties.Description="Task" />
2828
</sf:SfTextInputLayout>
2929

3030
<sf:SfTextInputLayout Hint="Completed">
3131
<CheckBox
3232
HorizontalOptions="End"
3333
IsChecked="{Binding IsCompleted}"
34-
SemanticProperties.Description="Status"
34+
SemanticProperties.Description="Completed"
3535
SemanticProperties.Hint="Indicates if this task is completed" />
3636
</sf:SfTextInputLayout>
3737

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Foundation;
2+
using UIKit;
3+
using System.Threading.Tasks;
4+
using DeveloperBalance.Services;
5+
6+
[assembly: Dependency(typeof(DeveloperBalance.SemanticScreenReaderAsyncImplementation))]
7+
namespace DeveloperBalance;
8+
9+
public class SemanticScreenReaderAsyncImplementation : IAsyncAnnouncement
10+
{
11+
static NSObject? Token;
12+
private TaskCompletionSource<bool>? announcementCompletionSource;
13+
14+
public void AddNotification()
15+
{
16+
if (Token != null)
17+
return;
18+
19+
Token = NSNotificationCenter.DefaultCenter.AddObserver(
20+
new NSString("UIAccessibilityAnnouncementDidFinishNotification"),
21+
AnnouncementDidFinish);
22+
}
23+
24+
public void RemoveNotification()
25+
{
26+
if (Token == null)
27+
return;
28+
29+
NSNotificationCenter.DefaultCenter.RemoveObserver(Token);
30+
Token = null;
31+
announcementCompletionSource = null;
32+
}
33+
34+
private void AnnouncementDidFinish(NSNotification notification)
35+
{
36+
announcementCompletionSource?.TrySetResult(true);
37+
}
38+
39+
public void Announce(string text)
40+
{
41+
if (!UIAccessibility.IsVoiceOverRunning)
42+
return;
43+
44+
UIAccessibility.PostNotification(UIAccessibilityPostNotification.Announcement, new NSString(text));
45+
}
46+
47+
public async Task AnnounceAsync(string text)
48+
{
49+
AddNotification();
50+
51+
if (!UIAccessibility.IsVoiceOverRunning)
52+
return;
53+
54+
announcementCompletionSource = new TaskCompletionSource<bool>();
55+
56+
await Task.Delay(100);
57+
UIAccessibility.PostNotification(UIAccessibilityPostNotification.Announcement, new NSString(text));
58+
59+
// Wait until the announcement is finished
60+
await announcementCompletionSource.Task;
61+
62+
RemoveNotification();
63+
}
64+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using DeveloperBalance;
3+
4+
namespace DeveloperBalance.Services;
5+
6+
public static class AnnouncementHelper
7+
{
8+
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
9+
public static async Task Announce(string recipeName)
10+
{
11+
#if IOS
12+
var _semanticScreenReader = App.ServiceProvider?.GetService<IAsyncAnnouncement>();
13+
if (_semanticScreenReader is not null)
14+
{
15+
await _semanticScreenReader.AnnounceAsync(recipeName);
16+
}
17+
#else
18+
SemanticScreenReader.Announce(recipeName);
19+
#endif
20+
}
21+
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
22+
23+
}

0 commit comments

Comments
 (0)