【Flutter】iOSネイティブでPasswordAutoFillを実装してみた
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でサポートされている値は次の表の非循環値です。
Dart | Swift |
null | nil |
bool | NSNumber(value: Bool) |
int | NSNumber(value: Int32) |
int, if 32 bits not enough | NSNumber(value: Int) |
double | NSNumber(value: Double) |
String | String |
Uint8List | FlutterStandardTypedData(bytes: Data) |
Int32List | TexFlutterStandardTypedData(int32: Data) |
Int64List | FlutterStandardTypedData(int64: Data) |
Float32List | FlutterStandardTypedData(float32: Data) |
Float64List | FlutterStandardTypedData(float64: Data) |
List | Array |
Map | Dictionary |
MessageCodecは拡張することができますが、MessageCodecのエンコード/デコードではByteDataを使用しているため、コールバックを引数として渡すことはできなさそうです。
なので、今回の実装ではEventChannelでイベントを受け取るようにしています。
PasswordAutoFillについて
PasswordAutoFillはログインやアカウント作成を簡単にするものです。
数回タップするだけで、新しいパスワードを作成して保存したり、既存のアカウントにログインできるようになります。
ログイン認証情報はデバイスに保存され、iCloud Keychainを使用して、デバイス間でこれらの認証情報を安全に同期できます。
また、SMSコードの自動入力も設定することができます。
実装
iOS側でPasswordAutoFillのView(ログインID入力欄とパスワード入力欄があるView)を作成し、それをMethodChannel, EventChannel, PlatformViewを活用してFlutterで表示します。
流れ
- iOSネイティブでViewを実装し、PlatformViewで表示
- EventChannelで入力欄の変更を通知
- MethodChannelでパスワード入力欄をクリア
- Flutter側で画面を作成
- 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ネイティブ側ではFlutterPlatformViewとFlutterPlatformViewFactoryに準拠したクラスを作成し、それを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つを行うことで使用することができるようになります。
- テキストフィールドにUITextContentTypeを設定
- AssociatedDomainを設定
テキストフィールドにUITextContentTypeを設定
まずはUITextContentTypeの設定をしていきます。
PasswordAutoFillに使用するUITextContentTypeは以下の4つがあります。
今回はusernameとpasswordを使用します。
UITextContentType | 用途 |
username | ログインIDの自動入力と保存(passwordまたはnewPasswordとセットで使用) |
password | パスワードの自動入力と保存(usernameとセットで使用) |
newPassword | 新しいパスワードの自動生成と保存(usernameとセットで使用) |
oneTimeCode | SMSコードの自動入力 |
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する方法が良いなと思いました。
ここまで読んでいただきありがとうございます。
この記事が誰かのお役に立てば幸いです。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
業務ではiOS開発に携わらせていただいています。 まだまだ分からないことだらけで、日々分からないことと戦いながら仕事をしている者です。 ブログ記事は暖かい目で見ていただけるとありがたいです。
おすすめ記事
immichを知ってほしい
2024.10.31