1. HOME
  2. ブログ
  3. IT技術
  4. 【Flutter】iOSネイティブでPasswordAutoFillを実装してみた

【Flutter】iOSネイティブでPasswordAutoFillを実装してみた

はじめに

iOSネイティブ側で実装したものをFlutterから呼び出す機能として、MethodChannel, EventChannel, PlatformViewがありますが、今回これらを活用してPasswordAutoFillを実装してみました。

デモ

※パスワード欄の録画ができませんでしたが、1回目にパスワードを手入力し、2回目にパスワードが自動入力されています。

環境

macOS: Venture 13.4
プロセッサ: Intel
Xcode: 14.3.1
Android Studio: Dolphin (2021.3.1)
Flutter SDK: 3.13.7

MethodChannelについて

MethodChannelは非同期メソッド呼び出しを使用してプラットフォーム プラグインと通信するための名前付きチャネルです。
MethodChannelを使うことでiOSネイティブ側のメソッドを呼び出すことができます。
詳しい使い方はドキュメント(Writing custom platform-specific code)に記載されています。

EventChannelについて

EventChannelはイベントストリームを使用してプラットフォームプラグインと通信するための名前付きチャネルです。
EventChannelを使うことでiOSネイティブのイベントをStreamでリッスンすることができます。

※ 使い方
iOSネイティブ側では、FlutterStreamHandlerに準拠したクラスを作成し、作成したクラスをsetStreamHandlerで登録します。
そしてFlutter側ではreceiveBroadcastStreamを使ってイベントをリッスンします。

PlatformViewについて

PlatformViewはFlutterアプリにネイティブビューを埋め込めるようにするものです。
iOSではUiKitViewを使うことでiOSネイティブのUIViewをWidget階層に埋め込むことができます。
詳しい使い方はドキュメント(Hosting native iOS views in your Flutter app with Platform Views
)に記載されています。

MessageCodecについて

iOSネイティブとFlutterのやり取りでは引数の値や戻り値をエンコード/デコードする必要があり、その際にMessageCodecを使用します。
MethodChannel、EventChannel、PlatformViewではMessageCodecが使われています。
標準でStandardMessageCodecが用意されていて、基本的な値はStandardMessageCodecを使えばエンコード/デコードができるようになっています。
StandardMessageCodecでサポートされている値は次の表の非循環値です。

DartSwift
nullnil
boolNSNumber(value: Bool)
intNSNumber(value: Int32)
int, if 32 bits not enoughNSNumber(value: Int)
doubleNSNumber(value: Double)
StringString
Uint8ListFlutterStandardTypedData(bytes: Data)
Int32ListTexFlutterStandardTypedData(int32: Data)
Int64ListFlutterStandardTypedData(int64: Data)
Float32ListFlutterStandardTypedData(float32: Data)
Float64ListFlutterStandardTypedData(float64: Data)
ListArray
MapDictionary

MessageCodecは拡張することができますが、MessageCodecのエンコード/デコードではByteDataを使用しているため、コールバックを引数として渡すことはできなさそうです。
なので、今回の実装ではEventChannelでイベントを受け取るようにしています。

PasswordAutoFillについて

PasswordAutoFillはログインやアカウント作成を簡単にするものです。
数回タップするだけで、新しいパスワードを作成して保存したり、既存のアカウントにログインできるようになります。
ログイン認証情報はデバイスに保存され、iCloud Keychainを使用して、デバイス間でこれらの認証情報を安全に同期できます。
また、SMSコードの自動入力も設定することができます。

実装

iOS側でPasswordAutoFillのView(ログインID入力欄とパスワード入力欄があるView)を作成し、それをMethodChannel, EventChannel, PlatformViewを活用してFlutterで表示します。

流れ

  1.  iOSネイティブでViewを実装し、PlatformViewで表示
  2. EventChannelで入力欄の変更を通知
  3. MethodChannelでパスワード入力欄をクリア
  4. Flutter側で画面を作成
  5. PasswordAutoFillの設定

iOSネイティブでViewを実装し、PlatformViewで表示

iOSネイティブ側でloginIdのテキストフィールドとpasswordのテキストフィールドを置いたViewを作成し、PlatformViewを使ってFlutter側にViewを埋め込みます。
まずはViewを作成します。
自分はGUIでUIを作成する方が好きなので、xibでLoginTextFieldViewというViewを作成しました。

次にxibに紐づくLoginTextFieldViewというUIViewクラスを作成します。

このコードではxibで作成したViewを取り出してUIViewに追加し、UIViewいっぱいに広がるように制約を設定しています。

Viewの作成ができたので、次にPlatFormViewを使ってFlutter側にViewを埋め込みます。
まずiOSネイティブ側の実装をします。
iOSネイティブ側ではFlutterPlatformViewFlutterPlatformViewFactoryに準拠したクラスを作成し、それをAppDelegateのdidFinishLaunchingOptionsで登録します。

このコードではFlutterPlatformViewとFlutterPlatformViewFactoryに準拠したクラスを作成しています。
LoginTextFieldPlatformViewではviewメソッドで先ほど作成したLoginTextFieldViewを返すようにし、LoginTextFieldPlatformViewFactoryではcreateメソッドでLoginTextFieldPlatformViewを返すようにします。
引数を設定した場合、createメソッドのargsにFlutter側で設定した引数が渡ってくるのですが、今回は特に不要なため使用していません。
また、引数を使用する場合はFlutterPlatformViewFactoryのcreateArgsCodecでMessageCodecを返す必要があります。

作成したFlutterPlatformViewFactoryをAppDelegateのdidFinishLaunchingOptionsで登録します。

このコードではregisterメソッドでLoginTextFieldPlatformViewFactoryを登録しています。
"login_view_plugin"と"login_text_field"はアプリ内で一意となるキーで、任意の値を設定しています。
Flutter側で"login_text_field"を使用して、登録されたファクトリーにUIViewの作成をリクエストすることができるようになります。

以上でiOSネイティブ側の実装は完了です。
次にFlutter側でLoginTextFieldViewを呼び出し、Widget階層に埋め込みます。

このコードではiOSネイティブ側で指定したキーを使ってUiKitViewを作成しています。
こうすることで、iOSネイティブのViewをWidget階層に埋め込むことができます。

※UiKitViewについて
- Viewの埋め込みは高コストの操作であるため、Flutterと同等の操作が可能な場合は避けることが推奨されている
- UiKitViewは利用可能なスペース全てを埋めるため、レイアウトの調整が必要
- Viewの構築は非同期で行われ、その間レイアウト制約を維持しながら何も描画されない
- FlutterのUIがプラットフォームスレッドから構成されることにより、OSやプラグイン・メッセージを処理するようなタスクと競合しパフォーマンスが悪くなる

EventChannelで入力欄の変更を通知

EventChannelを使ってログインIDの入力とパスワードの入力をFlutter側で受け取れるようにします。
まずは入力欄の変更を通知できるようDelegateを実装します。

このコードではログインID入力欄の値が変化した時に呼ぶeditingChangedLoginIdメソッドとパスワード入力欄の値が変化した時に呼ぶeditingChangedPasswordメソッドを定義しています。

Delegateの定義ができたので、次にEventChannelを使ってFlutter側で変更を受け取れるよう実装していきます。
まずiOSネイティブ側の実装をします。
iOSネイティブ側ではFlutterStreamHandlerに準拠したクラスを作成し、それをAppDelegateのdidFinishLaunchingOptionsで登録します。

このコードではFlutterStreamHandlerに準拠したLoginTextFieldStreamHandlerを作成し、extensionでDelegateに準拠させています。
Delegateのメソッドが呼ばれた時にonListenメソッドのコールバックを呼び出すことで、Flutter側でイベントを受け取れるようにしています。
onCancelメソッドはStreamから最後のリスナーが登録解除された時に呼ばれるメソッドで、今回は特に使用しないのでnilを返しています。

作成したLoginTextFieldStreamHandlerをAppDelegateのdidFinishLaunchingOptionsで登録します。

このコードではFlutterEventChannelを作成し、LoginTextFieldStreamHandlerを登録しています。
また、LoginTextFieldStreamHandlerはDelegateに準拠しているため、LoginTextFieldViewのdelegateに代入しています。
"login_text_field_view_handler"は識別子になっていて、Flutter側のEventChannelと同じ名前にする必要があります。

以上でiOSネイティブ側の実装は完了です。
次にFlutter側でイベントをリッスンするようにします。

このコードではreceiveBroadcastStreamメソッドを使ってイベントをリッスンするようにしています。
そして、受け取ったイベントをコールバックに渡しています。
'login_text_field_view_handler'は識別子で、iOSネイティブ側で実装したものと一致する必要があります。

MethodChannelでパスワード入力欄をクリア

今回、ログイン成功の遷移から戻った時にパスワードの入力をクリアにしたかったので、MethodChannelを使ってパスワード入力をクリアするメソッドを実装しました。
まずiOSネイティブ側の実装をします。
iOSネイティブ側ではパスワード欄をクリアするメソッドを実装し、それをMethodChannelで呼び出せるようにします。

LoginTextFieldViewにパスワード入力をクリアにするclearPasswordメソッドを追加しました。
このメソッドをMethodChannelで呼び出せるようにします。

このコードでは"flutter_password_auto_fill/login_text_field_method"という名前でMethodChannelを作成し、Flutter側の呼び出しが"clear_password"だったらclearPasswordメソッドを呼び出すという処理をしています。
"flutter_password_auto_fill/login_text_field_method"は識別子で必ず「/」が必要になります。
「/」を付け忘れていたらメソッドを呼び出すことができませんでした。

以上でiOSネイティブ側の実装は完了です。
次にFlutter側でメソッドを呼び出す処理を実装します。

このコードではiOSネイティブ側で設定した名前と一致するMethodChannelを作成し、invokeMethodでメソッドを呼び出しています。
親Widgetからここで定義したclearPasswordを呼び出す想定なので、LoginTextFieldStateから「_」をはずしています。

Flutter側で画面を作成

Viewの作成と繋ぎ込みができたので、次にログイン画面を作成していきます。
PasswordAutoFillは画面遷移時に保存処理が走るので、ログインボタンで画面遷移をするようにしています。

このコードではログイン画面を作成し、LoginTextFieldを配置しています。
UiKitViewは利用可能なスペースを全て埋めるため、LoginTextFieldはSizedBoxで高さ指定をしています。
そして、画面遷移から戻った時にclearPasswordメソッドを呼び出しています。

遷移しているLoginSuccessScreenは中心にテキストがあるだけの画面です。

PasswordAutoFillの設定

PasswordAutoFillの設定をしていきます。
PasswordAutoFillは以下の2つを行うことで使用することができるようになります。

  1. テキストフィールドにUITextContentTypeを設定
  2. AssociatedDomainを設定

テキストフィールドにUITextContentTypeを設定

まずはUITextContentTypeの設定をしていきます。
PasswordAutoFillに使用するUITextContentTypeは以下の4つがあります。
今回はusernameとpasswordを使用します。

UITextContentType用途
usernameログインIDの自動入力と保存(passwordまたはnewPasswordとセットで使用)
passwordパスワードの自動入力と保存(usernameとセットで使用)
newPassword新しいパスワードの自動生成と保存(usernameとセットで使用)
oneTimeCodeSMSコードの自動入力

xibのインスペクターからusernameとpasswordを設定しました。


AssociatedDomainを設定

次にAssociatedDomainを設定していきます。

まずapple-app-site-association(拡張子なし)ファイルを作成します。
証明書に記載されたApp ID Prefix(Team ID)とBundle IDを確認し、以下のようなapple-app-site-associationファイルを作成します。

apple-app-site-associationはWebサーバーの.well-knownディレクトリに配置する必要があります。
そして、ファイルURLは以下の形式と一致する必要があります。
https://<fully qualified domain>/.well-known/apple-app-site-association

今回はFirebaseのHostingを使ってapple-app-site-associationファイルを配置します。
新規でプロジェクトを作成し、ルートディレクトリで以下のコマンドを実行します。
$ firebase init hosting 

するとFirebaseのファイルが作成されます。
作成されたら.well-knownディレクトリを作成し、apple-app-site-associationファイルをディレクトリに追加します。
ディレクトリ構成は以下のようになりました。

次にapple-app-site-associationのContent-Typeをapplication/jsonにし、さらにFirebaseにapple-app-site-associationを自動作成させないにします。
firebase.jsonを以下のように変更します。

最後にプロジェクトのルートディレクトリで以下のコマンドを実行しデプロイします。
$ firebase deploy --only hosting

apple-app-site-associationファイルの配置ができたので、次にXcodeでAssociatedDomainの設定をします。
XcodeでSigning&Capabilitiesを開き、AssociatedDomainsのCapabilityを追加します。
追加したらDomainsに webcredentials:{Firebaseのドメイン} を追加します。
Firebaseのドメインは {プロジェクトID}.firebaseapp.com の形式になります。
追加するとentitlementsファイルが作成されます。

以上でPasswordAutoFillの設定完了です。

おわりに

今回iOSネイティブでPasswordAutoFillを実装してみました。

今回はViewをWidget階層に埋め込みましたが、iOSネイティブ側で画面を作成してFlutterViewControllerに直接Pushするという方法もあるみたいです。
FlutterViewControllerに直接Pushする方法だとUIViewをWidgetに変換する処理がないと思うので、パフォーマンスが良さそうです。
なので、一つの画面で完結するのであれば、FlutterViewControllerに直接Pushする方法が良いなと思いました。

ここまで読んでいただきありがとうございます。
この記事が誰かのお役に立てば幸いです。

書いた人はこんな人

いまむー(エンジニア)
いまむー(エンジニア)
業務ではiOS開発に携わらせていただいています。
まだまだ分からないことだらけで、日々分からないことと戦いながら仕事をしている者です。
ブログ記事は暖かい目で見ていただけるとありがたいです。

関連記事

採用情報

\ あの有名サービスに参画!? /

バックエンドエンジニア

\ クリエイティブの最前線 /

フロントエンドエンジニア

\ 世界を変える…! /

Androidエンジニア

\ みんなが使うアプリを創る /

iOSエンジニア