📱

React Nativeでもちゃんとやるアクセシビリティ

2024/12/17に公開

この記事はReact Native 全部俺 Advent Calendar 17目の記事です。

https://adventar.org/calendars/10741

このアドベントカレンダーについて

このアドベントカレンダーは @itome が全て書いています。

基本的にReact NativeおよびExpoの公式ドキュメントとソースコードを参照しながら書いていきます。誤植や編集依頼はXにお願いします。

アクセシビリティの重要性

アクセシビリティとは、障害の有無や年齢などに関係なく、誰もが使いやすいアプリを作ることです。スマートフォンは私たちの生活に欠かせないものになっており、金融や医療など重要なサービスもアプリ化が進んでいます。そのため、アクセシビリティへの対応は、単なる「あったら良い機能」ではなく、社会的な責任と言えます。
というふうに書くといかにも大上段に構えた感じがして身構えてしまいますが、世界の潜在的なアプリユーザーの15%はなんらかの障害を抱えていると言われているので、自分のアプリを多く使ってもらうためにも、アクセシビリティについてきちんと理解をしておくことが大切だと思います。

アクセシビリティ対応の種類

視覚障害のサポートでまず大切なのが、スクリーンリーダーへの対応です。画面の中身を音声で伝えるため、ボタンやリンクなどの操作可能な要素には、適切な読み上げテキストをつけていきます。また、文字がしっかり見えるようにテキストと背景のコントラスト比を確保したり、ユーザーが好みのフォントサイズに変更できるようにするのも重要です。画像には必ず代替テキストをつけて、見えなくても内容が伝わるようにしましょう。

運動機能に制限があるユーザーのために気をつけたいのが、タップ領域のサイズです。小さすぎると正確なタップが難しいので、最低でも44pt×44ptは確保したいところです。また、複雑なジェスチャーだけに頼らず、シンプルなタップでも同じことができるようにします。キーボードやスイッチコントロールなど、いろんな入力方法にも対応できるとベターです。

聴覚障害のあるユーザーのために、音声コンテンツには必ず字幕をつけます。通知音の代わりに振動や画面のフラッシュを使うなど、音以外での伝達手段も用意しておくと良いでしょう。音声認識や音声コマンドを実装する場合は、テキスト入力など別の方法でも同じことができるようにしておきます。

認知障害のサポートで大切なのは、シンプルで分かりやすいUIです。画面によって操作方法が大きく変わるのは避けたいところ。余白を十分にとって見やすくしたり、エラーが起きたときは何が問題なのかを分かりやすく説明したりします。時間制限のある操作では、制限時間を延ばせるようにするなど、焦らずに使えるような工夫が必要です。

React Nativeにおけるアクセシビリティ実装

React Nativeでは、iOSのVoiceOverとAndroidのTalkBackという2つの主要なスクリーンリーダーに対応する必要があります。以下では、具体的な実装方法を解説します。

基本的なアクセシビリティプロパティ

accessible と accessibilityLabel

// 良くない例
<TouchableOpacity onPress={onPress}>
  <Image source={require('./icon.png')} />
</TouchableOpacity>

// 良い例
<TouchableOpacity 
  accessible={true}
  accessibilityLabel="お気に入りに追加"
  onPress={onPress}
>
  <Image source={require('./icon.png')} />
</TouchableOpacity>

accessible={true}を設定すると、そのビューとその子要素が1つのアクセシビリティ要素としてグループ化されます。accessibilityLabelは、スクリーンリーダーが読み上げるテキストを指定します。

accessibilityRole と accessibilityState

要素の役割と状態を適切に伝えることで、スクリーンリーダーユーザーは操作の結果を予測できます:

function ToggleButton({ isOn, onToggle }) {
  return (
    <TouchableOpacity
      accessible={true}
      accessibilityRole="switch"
      accessibilityState={{
        checked: isOn,
        disabled: false
      }}
      onPress={onToggle}
    >
      <Text>{isOn ? 'ON' : 'OFF'}</Text>
    </TouchableOpacity>
  );
}

主なaccessibilityRoleの値:

  • button: ボタン
  • switch: トグルスイッチ
  • link: リンク
  • header: 見出し
  • search: 検索ボックス
  • image: 画像
  • text: 静的なテキスト
  • adjustable: スライダーなどの調整可能な要素

accessibilityValue と accessibilityHint

値を持つ要素(スライダーやプログレスバーなど)や、操作の結果を説明する必要がある場合に使用します:

function ProgressBar({ progress }) {
  return (
    <View
      accessible={true}
      accessibilityRole="progressbar"
      accessibilityValue={{
        min: 0,
        max: 100,
        now: progress,
        text: `${progress}%完了`
      }}
    >
      <View style={[styles.bar, { width: `${progress}%` }]} />
    </View>
  );
}

function BackButton() {
  return (
    <TouchableOpacity
      accessible={true}
      accessibilityLabel="戻る"
      accessibilityHint="前の画面に戻ります"
      onPress={onPress}
    >
      <Text></Text>
    </TouchableOpacity>
  );
}

プラットフォーム固有の機能

iOSのみの機能

Magic Tap

2本指でのダブルタップに反応する機能です。アプリの最も重要なアクションを割り当てます:

<View
  accessible={true}
  onMagicTap={() => {
    // 例:動画の再生/停止
    togglePlayback();
  }}
>
  <Video source={source} />
</View>

accessibilityLanguage

要素の読み上げに使用する言語を指定できます:

<Text
  accessible={true}
  accessibilityLabel="Pizza"
  accessibilityLanguage="it-IT"
>
  🍕
</Text>

Androidのみの機能

accessibilityLiveRegion

動的に変更される要素の通知方法を制御します:

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <View>
      <Button onPress={() => setCount(c => c + 1)} title="増やす" />
      <Text accessibilityLiveRegion="polite">
        現在の値: {count}
      </Text>
    </View>
  );
}
  • none: 変更を通知しない
  • polite: 現在の読み上げが終わってから通知
  • assertive: 即座に通知

アクセシビリティアクションの実装

複雑な操作をスクリーンリーダーユーザーに提供する場合、カスタムアクションを定義できます:

function EmailItem({ email, onDelete, onArchive }) {
  return (
    <View
      accessible={true}
      accessibilityLabel={`${email.from}からのメール: ${email.subject}`}
      accessibilityActions={[
        { name: 'delete', label: '削除' },
        { name: 'archive', label: 'アーカイブ' },
      ]}
      onAccessibilityAction={event => {
        switch (event.nativeEvent.actionName) {
          case 'delete':
            onDelete(email);
            break;
          case 'archive':
            onArchive(email);
            break;
        }
      }}
    >
      <Text>{email.subject}</Text>
    </View>
  );
}

スクリーンリーダーの検出

ユーザーがスクリーンリーダーを使用しているかどうかを検出して、UIを最適化できます:

import { AccessibilityInfo } from 'react-native';

function MyComponent() {
  const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false);

  useEffect(() => {
    // 初期状態を取得
    AccessibilityInfo.isScreenReaderEnabled().then(
      screenReaderEnabled => setIsScreenReaderEnabled(screenReaderEnabled)
    );

    // 状態変化を監視
    const subscription = AccessibilityInfo.addEventListener(
      'screenReaderChanged',
      screenReaderEnabled => setIsScreenReaderEnabled(screenReaderEnabled)
    );

    return () => {
      subscription.remove();
    };
  }, []);

  return (
    <View>
      {isScreenReaderEnabled ? (
        // スクリーンリーダー用の最適化されたUI
      ) : (
        // 通常のUI
      )}
    </View>
  );
}

テスト方法

iOSでのテスト

VoiceOverの基本設定

  1. 設定 → アクセシビリティ → VoiceOver から有効化
  2. ショートカットの設定:設定 → アクセシビリティ → アクセシビリティショートカット でVoiceOverを選択
    • ホームボタンのトリプルクリック(Face IDモデル以外)
    • サイドボタンのトリプルクリック(Face IDモデル)で切り替え可能に

VoiceOverの基本的な操作方法

テストする際は、以下の基本的なジェスチャーを覚えておくと効率的です:

  • 1本指で画面を触る:要素の読み上げ
  • 1本指で右にフリック:次の要素に移動
  • 1本指で左にフリック:前の要素に移動
  • 2本指で下にフリック:選択している要素から順に読み上げ
  • 2回タップ:選択している要素をタップ(ボタンの押下など)
  • 3本指で上/下にフリック:スクロール
  • 2本指でZ形にドラッグ:直前の操作をキャンセル

Xcodeでのテスト

Xcodeの場合、Accessibility Inspectorを使用すると効率的にテストできます

  1. Xcodeを起動
  2. Xcode → Open Developer Tool → Accessibility Inspector
  3. 対象のデバイス/シミュレータを選択
  4. インスペクタを使って各要素を検証:
    • アクセシビリティラベル
    • ロール
    • トレイト(特性)
    • アクション

まとめ

アクセシビリティ対応は、誰もが使いやすいアプリを作るために不可欠な要素です。React Nativeは豊富なアクセシビリティAPIを提供しており、適切に実装することで、多様なユーザーに対応したアプリを開発できます。

Discussion