👋

FlutterのTextFieldでiOSのペースト許可要求を回避する方法

2024/08/22に公開

概要

iOS 16以降だと、他のアプリでコピーした内容をペーストしようとすると、ペーストの許可を求めるアラートが表示されるようになっています。
これは、本来アプリケーションからクリップボードを参照しようとしたときに表示されるものであって、ユーザ自身の操作によってTextField上にペーストしようとしたときには表示されません。

これまで、Flutterのテキスト入力ではFlutter上で描画されるコンテキストメニューであるために起こっていた問題でしたが、OS標準のコンテキストメニューを使用できるようになりました。
それによって、ペースト許可のダイアログを回避することが可能です。

Stable版だと、 Flutter 3.24.0 以降で利用できます。

Native ios context menu by @justinmc in 143002

https://docs.flutter.dev/release/release-notes/release-notes-3.24.0

実装方法

システムコンテキストメニューを使用してペースト許可を求められることのないTextFieldの実装例は、リファレンスのサンプルコードでも公開されています。

This example shows how to create a TextField that uses the system context menu where supported and does not show a system notification when the user presses the "Paste" button.

https://api.flutter.dev/flutter/widgets/SystemContextMenu-class.html

TextFieldcontextMenuBuilder を次のように実装します。

TextField(
    contextMenuBuilder:
        (BuildContext context, EditableTextState editableTextState) {
        // 可能な場合はシステムコンテキストメニューを利用する
        if (SystemContextMenu.isSupported(context)) {
            return SystemContextMenu.editableText(
                editableTextState: editableTextState,
            );
        }

        // flutter-rendered なコンテキストメニューを利用する
        return AdaptiveTextSelectionToolbar.editableText(
            editableTextState: editableTextState,
        );
    },
)

動作させてみると、まず見た目も少し異なります。

従来のContextMenu SystemContextMenu

普段見慣れているUIが表示されました。
このままPasteをタップすると、SystemContextMenuのほうであれば許可を求められることなくテキストを貼り付けることができます。

もちろん、 CupertinoTextField でもそのまま利用可能です。

日本語化

メニュー項目の日本語化の方法は、以下のとおりです。

従来のコンテキストメニューの場合は、 MaterialApplocale あたりの言語設定をもとに反映されていました。
システムコンテキストメニューの場合は、Xcodeから Runnner/Info.plist へ Localizations の項目を追加し、そこへ Japanece を追加します。

いずれも、すでに日本語対応されていれば特に必要ないものだと思います。

さいごに

ユーザ体験においてハードルの高いテキスト入力。以前は、ペーストボタンを置いて、実際にテキストが取得されるまでの時間を計測し、それが一定以上だったら常に許可する設定を促すヒントを表示するような実装をしたこともありました。

ネイティブと比較してデメリットだった点がひとつ解消されて、ありがたい限りです。

デモ画面のコード
import 'package:flutter/material.dart';
import '../style/text.dart';

class CreateEventPage extends StatelessWidget {
  const CreateEventPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('SystemContextMenu Test')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Row(
            children: [
              SizedBox(
                width: 144,
                child: StyledText.bodySmall('Normal'),
              ),
              const SizedBox(height: 16),
              Expanded(
                child: TextField(
                  onTapOutside: (_) =>
                      FocusManager.instance.primaryFocus?.unfocus(),
                ),
              ),
            ],
          ),
          const SizedBox(height: 24),
          Row(
            children: [
              SizedBox(
                width: 144,
                child: StyledText.bodySmall('SystemContextMenu'),
              ),
              const SizedBox(height: 16),
              Expanded(
                child: TextField(
                  onTapOutside: (_) =>
                      FocusManager.instance.primaryFocus?.unfocus(),
                  contextMenuBuilder: (BuildContext context,
                      EditableTextState editableTextState) {
                    if (SystemContextMenu.isSupported(context)) {
                      return SystemContextMenu.editableText(
                        editableTextState: editableTextState,
                      );
                    }

                    return AdaptiveTextSelectionToolbar.editableText(
                      editableTextState: editableTextState,
                    );
                  },
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

Discussion