• トップ
  • ブログ一覧
  • 【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する方法が良いなと思いました。

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

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    いまむー(エンジニア)

    いまむー(エンジニア)

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background