• トップ
  • ブログ一覧
  • 【Flutter】iOSネイティブでPasswordAutoFillを実装してみた
  • 【Flutter】iOSネイティブでPasswordAutoFillを実装してみた

    いまむー(エンジニア)いまむー(エンジニア)
    2023.11.01

    IT技術

    はじめに

    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クラスを作成します。

    1import UIKit
    2
    3class LoginTextFieldView: UIView {
    4    init() {
    5        super.init(frame: .zero)
    6        // xibで作成したViewをUIViewに追加
    7        if let view = Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as? UIView {
    8            self.addSubview(view)
    9
    10            // xibで作成したViewがUIViewいっぱいに広がるよう制約を設定
    11            view.translatesAutoresizingMaskIntoConstraints = false
    12            NSLayoutConstraint.activate([
    13                view.topAnchor.constraint(equalTo: self.topAnchor),
    14                view.leftAnchor.constraint(equalTo: self.leftAnchor),
    15                view.rightAnchor.constraint(equalTo: self.rightAnchor),
    16                view.bottomAnchor.constraint(equalTo: self.bottomAnchor)
    17            ])
    18        }
    19    }
    20
    21    required init?(coder: NSCoder) {
    22        fatalError("init(coder:) has not been implemented")
    23    }
    24}

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

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

    1class LoginTextFieldPlatformView: NSObject, FlutterPlatformView {
    2    private let loginTextFieldView: LoginTextFieldView
    3
    4    init(_ loginTextFieldView: LoginTextFieldView) {
    5        self.loginTextFieldView = loginTextFieldView
    6        super.init()
    7    }
    8
    9    func view() -> UIView {
    10        return loginTextFieldView
    11    }
    12}
    13
    14class LoginTextFieldPlatformViewFactory: NSObject, FlutterPlatformViewFactory {
    15    private let loginTextFieldPlatformView: LoginTextFieldPlatformView
    16
    17    init(_ loginTextFieldPlatformView: LoginTextFieldPlatformView) {
    18        self.loginTextFieldPlatformView = loginTextFieldPlatformView
    19        super.init()
    20    }
    21
    22    func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
    23        return loginTextFieldPlatformView
    24    }
    25}

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

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

    1import UIKit
    2import Flutter
    3
    4@UIApplicationMain
    5@objc class AppDelegate: FlutterAppDelegate {
    6    override func application(
    7        _ application: UIApplication,
    8        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    9    ) -> Bool {
    10        let loginTextFieldView = LoginTextFieldView() // ←追加
    11
    12        GeneratedPluginRegistrant.register(with: self)
    13
    14        let loginTextFieldPlatformView = LoginTextFieldPlatformView(loginTextFieldView) // ←追加
    15        let loginTextFieldPlatformViewFactory = LoginTextFieldPlatformViewFactory(loginTextFieldPlatformView) // ←追加
    16        self.registrar(forPlugin: "login_view_plugin")!.register(loginTextFieldPlatformViewFactory, withId: "login_text_field") // ←追加
    17
    18        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    19    }
    20}

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

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

    1import 'package:flutter/material.dart';
    2
    3class LoginTextField extends StatefulWidget {
    4    const LoginTextField({
    5        super.key,
    6    });
    7
    8    @override
    9    State<LoginTextField> createState() => LoginTextFieldState();
    10}
    11
    12class LoginTextFieldState extends State<LoginTextField> {
    13    @override
    14    Widget build(BuildContext context) {
    15        const viewType = 'login_text_field';
    16        return const UiKitView(viewType: viewType);
    17    }
    18}

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

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

    EventChannelで入力欄の変更を通知

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

    1import Flutter
    2import UIKit
    3
    4// ↓追加
    5protocol LoginTextFieldViewDelegate: AnyObject {
    6    func editingChangedLoginId(text: String) -> Void
    7    func editingChangedPassword(text: String) -> Void
    8}
    9
    10class LoginTextFieldView: UIView {
    11
    12    weak var delegate: LoginTextFieldViewDelegate? // ←追加
    13
    14    ...省略
    15
    16    // ↓追加
    17    @IBAction private func editingChangedLoginIdTextField(_ sender: UITextField) {
    18        delegate?.editingChangedLoginId(text: sender.text ?? "")
    19    }
    20
    21    // ↓追加
    22    @IBAction private func editingChangedPasswordTextField(_ sender: UITextField) {
    23        delegate?.editingChangedPassword(text: sender.text ?? "")
    24    }
    25}

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

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

    1import Flutter
    2
    3class LoginTextFieldStreamHandler: NSObject, FlutterStreamHandler {
    4    private var eventSink: FlutterEventSink?
    5
    6    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    7        eventSink = events
    8        return nil
    9    }
    10
    11    func onCancel(withArguments arguments: Any?) -> FlutterError? {
    12        return nil
    13    }
    14}
    15
    16extension LoginTextFieldStreamHandler: LoginTextFieldViewDelegate {
    17    func editingChangedLoginId(text: String) {
    18        eventSink?(["loginId": text])
    19    }
    20
    21    func editingChangedPassword(text: String) {
    22        eventSink?(["password": text])
    23    }
    24}

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

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

    1import UIKit
    2import Flutter
    3
    4@UIApplicationMain
    5@objc class AppDelegate: FlutterAppDelegate {
    6    override func application(
    7        _ application: UIApplication,
    8        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    9    ) -> Bool {
    10        let loginTextFieldView = LoginTextFieldView()
    11
    12        let controller = window.rootViewController as! FlutterViewController // ←追加
    13        let loginTextFieldStreamHandler = LoginTextFieldStreamHandler() // ←追加
    14        // イベントチャネルに任意の名前を付け、controllerのbinaryMessengerを使って初期化
    15        let loginIdEventChannel = FlutterEventChannel(name:  "login_text_field_view_handler", binaryMessenger: controller.binaryMessenger) // ←追加
    16        loginIdEventChannel.setStreamHandler(loginTextFieldStreamHandler) // ←追加
    17
    18        loginTextFieldView.delegate = loginTextFieldStreamHandler // ←追加
    19
    20       ...省略
    21    }
    22}

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

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

    1import 'package:flutter/material.dart';
    2import 'package:flutter/services.dart';
    3
    4class LoginTextField extends StatefulWidget {
    5    final Function(String) onChangedLoginId; // ←追加
    6    final Function(String) onChangedPassword; // ←追加
    7
    8    const LoginTextField({
    9        super.key,
    10        required this.onChangedLoginId, // ←追加
    11        required this.onChangedPassword, // ←追加
    12    });
    13
    14    @override
    15    State<LoginTextField> createState() => LoginTextFieldState();
    16}
    17
    18class LoginTextFieldState extends State<LoginTextField> {
    19    static const _loginTextFieldEventChannel =
    20        EventChannel('login_text_field_view_handler'); // ←追加
    21
    22    // ↓追加
    23    @override
    24    void initState() {
    25        _loginTextFieldEventChannel.receiveBroadcastStream().listen((event) {
    26            const loginIdKey = 'loginId';
    27            const passwordKey = 'password';
    28            if (event.containsKey(loginIdKey)) {
    29                widget.onChangedLoginId(event[loginIdKey]);
    30            } else if (event.containsKey(passwordKey)) {
    31                widget.onChangedPassword(event[passwordKey]);
    32            }
    33        });
    34        super.initState();
    35    }
    36
    37    ...省略
    38}

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

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

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

    1class LoginTextFieldView: UIView {
    2    @IBOutlet private weak var passwordTextField: UITextField! // ←追加
    3
    4    ...省略
    5
    6    // ↓追加
    7    func clearPassword() {
    8        passwordTextField.text = ""
    9    }
    10}

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

    1import UIKit
    2import Flutter
    3
    4@UIApplicationMain
    5@objc class AppDelegate: FlutterAppDelegate {
    6    override func application(
    7        _ application: UIApplication,
    8        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    9    ) -> Bool {
    10        let loginTextFieldView = LoginTextFieldView()
    11
    12        let controller = window.rootViewController as! FlutterViewController
    13        // ↓追加
    14        let methodChannel = FlutterMethodChannel(name: "flutter_password_auto_fill/login_text_field_method", binaryMessenger: controller.binaryMessenger)
    15        methodChannel.setMethodCallHandler { methodCall, result in
    16            guard methodCall.method == "clear_password" else {
    17                return result(FlutterMethodNotImplemented)
    18            }
    19            loginTextFieldView.clearPassword()
    20            return result(nil)
    21        }
    22
    23       ...省略
    24    }
    25}

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

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

    1class LoginTextFieldState extends State<LoginTextField> {
    2    static const _loginTextFieldEventChannel =
    3        EventChannel('login_text_field_view_handler');
    4    static const _loginTextFieldMethodChannel =
    5        MethodChannel('flutter_password_auto_fill/login_text_field_method'); // ←追加
    6
    7    // ↓追加
    8    Future<void> clearPassword() async {
    9        await _loginTextFieldMethodChannel.invokeMethod('clear_password');
    10    }
    11
    12        ...省略
    13}

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

    Flutter側で画面を作成

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

    1import 'package:flutter/material.dart';
    2import 'package:flutter_password_auto_fill/login/login_text_field.dart';
    3import 'package:flutter_password_auto_fill/login_success/login_success_screen.dart';
    4
    5class LoginScreen extends StatelessWidget {
    6   const LoginScreen({super.key});
    7
    8   @override
    9   Widget build(BuildContext context) {
    10       final loginTextFieldKey = GlobalObjectKey<LoginTextFieldState>(context);
    11
    12       return Scaffold(
    13           appBar: AppBar(
    14           title: const Text('ログイン'),
    15           ),
    16           body: SafeArea(
    17               child: Padding(
    18                   padding: const EdgeInsets.all(32.0),
    19                   child: Column(
    20                       children: [
    21                           const Icon(
    22                               Icons.lock_outline,
    23                               size: 50,
    24                               color: Colors.black45,
    25                           ),
    26                           const SizedBox(height: 32),
    27                           SizedBox(
    28                               height: 88,
    29                               child: LoginTextField( // ←作成したViewを配置
    30                                   key: loginTextFieldKey,
    31                                   onChangedLoginId: (text) {
    32                                       print('loginId: $text');
    33                                   },
    34                                   onChangedPassword: (text) {
    35                                       print('password: $text');
    36                                   },
    37                               ),
    38                           ),
    39                           const SizedBox(height: 24),
    40                           ElevatedButton(
    41                               onPressed: () async {
    42                                   await Navigator.push(
    43                                       context,
    44                                       MaterialPageRoute(
    45                                           builder: (context) {
    46                                               return const LoginSuccessScreen();
    47                                           },
    48                                       ),
    49                                   );
    50                                   loginTextFieldKey.currentState?.clearPassword(); // ←画面を戻った時にパスワードをクリア
    51                               },
    52                               child: const Text(
    53                                   'ログイン',
    54                                   style: TextStyle(
    55                                       fontSize: 16,
    56                                       fontWeight: FontWeight.bold,
    57                                   ),
    58                               ),
    59                           ),
    60                       ],
    61                   ),
    62               ),
    63           ),
    64       );
    65   }
    66}

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

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

    1import 'package:flutter/material.dart';
    2
    3class LoginSuccessScreen extends StatelessWidget {
    4 const LoginSuccessScreen({super.key});
    5
    6 @override
    7 Widget build(BuildContext context) {
    8   return Scaffold(
    9     appBar: AppBar(
    10       title: const Text('ログイン成功'),
    11     ),
    12     body: const SafeArea(
    13       child: Center(
    14         child: Text('ログイン成功'),
    15       ),
    16     ),
    17   );
    18 }
    19}

    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ファイルを作成します。

    1{
    2    "webcredentials": {
    3        "apps": [
    4            "{App ID Prefix(Team ID)}.{Bundle ID}"
    5        ]
    6    }
    7}

    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ファイルをディレクトリに追加します。
    ディレクトリ構成は以下のようになりました。

    1プロジェクト
    2├── .firebase
    3├── public/
    4│   ├── .well-known
    5│   │   └── apple-app-site-association
    6│   ├── 404.html
    7│   └── index.html
    8├── .firebaserc
    9├── .gitignore
    10└── firebase.json

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

    1{
    2    "hosting": {
    3        "public": "public",
    4        "ignore": [
    5            "firebase.json",
    6            "**/.*",
    7            "**/node_modules/*"
    8        ],
    9        "headers": [
    10            {
    11                "source": "/.well-known/apple-app-site-association",
    12                "headers": [
    13                    {
    14                        "key": "Content-Type",
    15                        "value": "application/json"
    16                    }
    17                ]
    18            }
    19        ],
    20        "appAssociation": "NONE"
    21    }
    22}

    最後にプロジェクトのルートディレクトリで以下のコマンドを実行しデプロイします。
    $ 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する方法が良いなと思いました。

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

    いまむー(エンジニア)

    いまむー(エンジニア)

    おすすめ記事