- 投稿日:2020-04-04T22:51:43+09:00
iOS開発でDEV環境を構築する
iOSアプリを運用していくにあたって、アプリの配布やサーバーサイドとの連携は必須となります。
その際にクライアント側でDevやStaging、Releaseなど環境を使い分けたいケースは必ず出てきます。
今回はXcode上でDev環境を作る一連の流れをまとめてみました。
複数の環境を追加したい場合もこの流れで同じことを繰り返していけば良いので、簡単です。この記事ではConfigrationを変更することで、DEV環境を作ります。
他にもTargetsを増やす方法やxcconfigファイルを使用する方法もあるので、チェックしてみてください。ProjectのConfigurationのNameをDebug -> Developに変更
単純にリネームです。
3つ以上環境使い分けたいケースでは、ここで追加をしてください。Preprocessor Marcos(Project -> Build Setting)で、DEBUG=1 -> Develop=1に変更
- Preprocessor Marcosとは
- 環境ごとに条件分岐できるようにするためのXcodeの機能
- SwiftでPreprocessor Macrosを使う
Other Swift flagの設定
#if DEVELOP
とかできるようにするために行う。
-D <flag名>
の形式で設定する。
複数やる場合は、-D <flag名> -D <flag名>
DEV sheme作成
sheme選択画面でDEV版が選べるようになります。
↓こんな感じ。Manage schemeを選択。
Duplicate schemeをクリックして、既存のshemeを複製。
名前が
copy of <プロジェクト名>
となっていると思うので、本番と見分けがつくように名前を設定。Run, Test, Profile, ArchiveのBuild configurationをチェックする。
今回はRun, ArchiveのときにDevelopを向いてるようにしました。
以上の作業で基本的なDEV環境作成はできていて、別々のアプリとして認識することが可能となりました。
次からは見た目もわかりやすくしていく作業です。Product nameを変更
Targets -> Build Setting -> Product Nameから
$(TARGET_NAME)DEV
に変更。アプリのアイコンを変更
まずはDev用のAppIconを作成し、
AppIcon_dev
と命名します。
Targets -> Build Settings -> Asset Catalog App Icon Set Nameを開き、Developのところを、
AppIcon_dev
とします。
FirebaseのDev版作成
iOS開発において、分析や配布のためにFirebaseを使用しているケースは多々あると思います。
DEV環境を作った際にはFirebaseでもその対応をする必要があります。まずはFirebase上で、DevとReleaseでそれぞれプロジェクトを作成します。
その際にどちらもGoogleService-info.plistファイルを保存しておきます。
Targets -> Build Phase -> 左上のプラスボタンから新規でRun Scriptを作成します。
CONFIGURATIONに応じて、参照するGoogleService-Inforファイルを切り替えて、BUILT_PRODUCTS_DIRディレクトリにコピペしています。if [ "${CONFIGURATION}" == "Release" ]; then cp "${PROJECT_DIR}/${PROJECT_NAME}/GoogleService/GoogleService-Info.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" else cp "${PROJECT_DIR}/${PROJECT_NAME}/GoogleService/GoogleService-Info-Develop.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" fiハマったところ
アプリ起動後、画面を読み込む部分でクラッシュしました。
原因はDEV環境内で、Storyboard内のUIViewControllerのModuleが本番環境を向いていることでした。
Inherit Module From Targetにチェックをつけると、Moduleを指定しなくても、よしなにやってくれるので、クラッシュする場合は該当のVCのこの箇所をチェックすると良いです。
参考
error: Could not get GOOGLE_APP_ID in Google Services file from build environment
これはFabric初期化のタイミングでGoogleService-Info.plistが生成されていないのが原因です。
Fabric導入した際に、"${PODS_ROOT}/Fabric/run"
をRun Scriptとして追加したと思いますが、Firebase ConfigurationのScriptはそれより前に実行されるようにしてください。
参考リンク
https://qiita.com/Todate/items/a2e6a26731c79bd23e02#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%82%A2%E3%82%A4%E3%82%B3%E3%83%B3
https://qiita.com/KazaKago/items/aacf6eaec65b6d4244d0
- 投稿日:2020-04-04T21:59:06+09:00
iosアプリ開発 初心者
はじめに
これからiosアプリを開発していくにあたって日々学んだことをアウトプットしていきます。
少しでも参考になれたらと思います。Mac
AndroidアプリはMacとWindowsどちらでも開発可能ですが、iPhone用のアプリを開発するにはMacが必要です。
これからアプリを開発していこうと考えている方はMacのPCを
買うことをおすすめします。Xode
XcodeはAppleが無料で提供しているiosアプリ開発では王道のツールです。
iPhone、iPad、Mac、Apple Watchのアプリを開発できます。※XcodeはMacでのみ動作しますが、最新バージョンのXcodeをインストールするためには、MacのOSも最新バージョンである必要があります。
Swift
SwiftはAppleが開発した新しいプログラミング言語で2014年から使わています。
対象としているOSは、MacOSとiOSで、PhoneやiPadなどの端末で使えるアプリ、パソコン向けのアプリを開発することができます。
これまではObjective-C使われていましたが、スピード・安定性・コスト・メンテナンス面から考えるとSwiftが優れています。
- 投稿日:2020-04-04T21:25:59+09:00
[Flutter] Dribbbleデザイン、そのまま作ろう その15
こんにちは 皆さん!Dreamwalkerです。
皆さんはFlutterを使って楽しんで開発進んでいますか?今回は15回目の私がDribbbleのサイトの中で、
気に入れたデザインをFlutterで作ってみた。。ということになります。Dribbbleというサイトは色んなデザイナーさん達が色んなデザインを載せていて
デザインをの知識というか、デザイナーじゃない人でも、開発に参考になったり、役に立ったりするサイトです。まあ、デザインに対して、色んなサイトありますけれど私はDribbbleを愛用してます。今回のデザイン
今回は15番目の「DribbbleのデザインをFlutterでやってみた」になります。
Flight Ticket
右側の席を選ぶ画面を作ってみたいと思います。
https://dribbble.com/shots/10862680-Flight-Ticket/attachments/2517650?mode=media結果
必要なライブラリー
1.
google_fonts: ^0.3.10
データモデル
seat.dartclass Seat { final bool isAvailable; Seat({this.isAvailable}); }2次元配列
main.dart_tmp = List.generate(_column, (index) => List<Seat>.generate(_row, (index) => Seat(isAvailable: random.nextBool()))); _tmp.forEach((element) { element.forEach((e) { print(e.isAvailable); }); });全てのコード
main。
main.dartimport 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutternotebook5th/generated/i18n.dart'; import 'package:google_fonts/google_fonts.dart'; class Seat { final bool isAvailable; Seat({this.isAvailable}); } class FlightTicketApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: TicketPage(), ); } } class TicketPage extends StatefulWidget { @override _TicketPageState createState() => _TicketPageState(); } class _TicketPageState extends State<TicketPage> { Color appColor = Color(0xff3959b4); // 57 89 180 dec to hex 39 59 b4 List<Seat> _item; int _row = 10; int _column = 14; List<List<Seat>> _tmp; @override void initState() { // TODO: implement initState Random random = Random(); //TODO: One Dimension Array _item = List<Seat>.generate(10, (index) => Seat(isAvailable: random.nextBool())); _item.forEach((element) { print(element.isAvailable); }); //TODO : 2 Dimension Array _tmp = List.generate(_column, (index) => List<Seat>.generate(_row, (index) => Seat(isAvailable: random.nextBool()))); _tmp.forEach((element) { element.forEach((e) { print(e.isAvailable); }); }); super.initState(); } List<Widget> _buildSeatTitle() { return <Widget>[ Expanded( child: Container(), ), Expanded( child: Container( child: Center( child: Text( "A", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container( child: Center( child: Text( "B", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container( child: Center( child: Text( "C", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container(), ), Expanded( child: Container( child: Center( child: Text( "D", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container( child: Center( child: Text( "E", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container( child: Center( child: Text( "F", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container( child: Center( child: Text( "G", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container(), ), Expanded( child: Container( child: Center( child: Text( "H", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container( child: Center( child: Text( "I", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container( child: Center( child: Text( "J", style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), ), ), ), ), Expanded( child: Container(), ), ]; } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Stack( children: <Widget>[ Positioned( left: 8, right: 24, top: 0, child: Row( mainAxisSize: MainAxisSize.min, children: <Widget>[ Icon(Icons.keyboard_arrow_left), Spacer(), Text( "Your Ticket", style: GoogleFonts.montserrat(fontSize: 20), ), Spacer(), ], ), ), Positioned( left: 0, right: 0, top: 48, bottom: 0, child: Container( decoration: BoxDecoration( color: appColor, borderRadius: BorderRadius.circular(24), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Expanded( flex: 2, child: Padding( padding: const EdgeInsets.only(left: 24, top: 16), child: Text( "Select Seats", style: GoogleFonts.montserrat(color: Colors.white, fontWeight: FontWeight.w500, fontSize: 16), ), ), ), Expanded( flex: 28, child: Column( children: <Widget>[ Expanded( flex: 1, child: Row( children: _buildSeatTitle(), ), ), Expanded( flex: 15, child: ListView.builder( itemCount: _tmp.length, itemBuilder: (context, index) { List<Seat> _seatItems = _tmp[index]; int idx = 30 + index; return Padding( padding: const EdgeInsets.all(6.0), child: Row( children: <Widget>[ Expanded( child: Text( idx.toString(), style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white), )), Expanded( child: _seatItems[0].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: _seatItems[1].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: _seatItems[2].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: Container(), ), Expanded( child: _seatItems[3].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: _seatItems[4].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: _seatItems[5].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: _seatItems[6].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded(child: Container()), Expanded( child: _seatItems[7].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: _seatItems[8].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: _seatItems[9].isAvailable ? Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ) : Container( margin: EdgeInsets.symmetric(horizontal: 3), height: 20, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), ), Expanded( child: Container(), ), ], ), ); }), ) ], ), ), Expanded( flex: 4, child: Column( children: <Widget>[ Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: <Widget>[ Container( height: 24, width: 24, decoration: BoxDecoration( color: Colors.indigoAccent, borderRadius: BorderRadius.circular(6)), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( "Choosen", style: GoogleFonts.montserrat(color: Colors.white, fontSize: 11), ), ), Spacer(), Container( height: 24, width: 24, decoration: BoxDecoration( border: Border.all(color: Colors.white), borderRadius: BorderRadius.circular(6)), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( "Available", style: GoogleFonts.montserrat(color: Colors.white, fontSize: 11), ), ), Spacer(), Container( height: 24, width: 24, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6)), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( "Blocked", style: GoogleFonts.montserrat(color: Colors.white, fontSize: 11), ), ), ], ), ), ), Expanded( child: Padding( padding: const EdgeInsets.only(right: 24, top: 8), child: Align( alignment: Alignment.centerRight, child: MaterialButton( elevation: 8, color: appColor, onPressed: () {}, child: Text( "Confirm", style: GoogleFonts.montserrat( fontSize: 12, color: Colors.white, ), ), ), ), ), ), ], ), ), Expanded(flex: 4, child: Container()), ], ), ), ), Positioned( bottom: 0, right: 0, left: 0, child: Container( height: 64, padding: EdgeInsets.symmetric(horizontal: 24), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), )), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ IconButton( icon: Icon(Icons.home), onPressed: () {}, ), IconButton( icon: Icon(Icons.card_giftcard), onPressed: () {}, ), IconButton( icon: Icon(Icons.style), color: Colors.indigoAccent, onPressed: () {}, ), IconButton( icon: Icon(Icons.person_outline), onPressed: () {}, ) ], ), ), ) ], ), ), ); } }終わりに
今回も読んでくださってありがとうございます。
- 投稿日:2020-04-04T20:46:42+09:00
Swift UITableViewの編集モードでのセル並び替え機能
canMoveRowAtを呼び、trueにする。
sample.swiftoverride func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { return true }moveRowAtクラスに並び替えの細かい操作を記入する。
sample.swiftoverride func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { //並び替え中に他に行いたい操作を書く(データの保存など) }上の2つのクラスどちらも呼び出さないと並び替えをすることは可能にならない。
canEditRowクラスを呼び出さなくても編集モード中に並び替えだけは可能になる。
- 投稿日:2020-04-04T20:33:03+09:00
【iOS】Firebase Cloud Messagingで利用するAPNs認証キー・証明書の作り方
以前開発したときは、APNs証明書だけだったのに知らぬうちに2つの方法ができていたので備忘録です。
そもそもAPNsとは?
Apple Push Notification Service(以下、APNs)は、iOSやtvOS、MacOS上で実行されているアプリケーションに対して開発者側から通知を送るためのAppleによるサービスのことを指します。
誤解されがちなのは、Firebase Cloud Messaging(以下、FCM)などの通知を行えるサービスと各アプリケーションが1対1で繋がっているようにとらわれがちですが、Appleデバイスを対象に通知を送付する際は必ずAPNsを介して送付されています。当たり前ですが、APNsに対してどんなリクエストでも送れてしまったら意図しない通知がユーザーの手元に届いてしまいますので、サーバー側とAPNs間のセキュアな通信を担保するために証明書または、認証キーが必要です。
認証キーと証明書
現在は、Appleデバイスに対してPush通知を送るために用いる認証方法が2つ存在しています。
記事によってはどちらかのみ記載している例があるため、それぞれ記載します。Apple Push Notification Service SSL
今まで、Push通知を行う際に用いられてた証明書タイプの認証方法です。
アプリごとに証明書を発行する必要があり、発行から1年間の有効期限付きです。
また、SandboxとProductionそれぞれの証明書を発行する必要があります。
Sandboxでは上手く行っていたのに、AdhocやReleaseのアプリに通知が飛ばないとかは、証明書の登録漏れとかが原因だったりすることが多いです。Apple Push Notification Authentication Key
こちらが新しい方法。(新しいといってもだいぶ前からなんですが、Push関連やることが少なかったので浦島太郎でした。)
アプリごとに証明書を用意する必要がなくベンダー共有の認証キーです。
有効期限がないため、更新する手間がないのがメリットです。基本的には、こちらの認証方法が現在では推奨されています。
発行方法
Apple Push Notification Authentication Key
Apple Developer Programのページにアクセスし、左側の「Program Resources」以下に表示されている「Certtificates, Identifiers & Profiles」をクリックします。
次に「Keys」にアクセスし、Keys横のプラスボタンをクリックします。
Keyの登録画面になるので、Keyの名称とAPNsのチェックボックスにチェックを入れてContinueをクリックします。
再度確認画面が表示されるのでRegisterをクリックするとKeyを生成することができます。
生成後、p8形式のキーのダウンロードが可能になります。生成されたキーは1度だけダウンロードできます。
1度ダウンロードした後、再ダウンロードすることはできませんので注意が必要です。Apple Push Notification Service SSL
こちらが古い方法。
Authentication Keyと異なりいくつかのステップがあります。(Developer Programのページで完結しないのが面倒)1.Certificate Signing Requestの発行
Certificate Signing Reques(以下、CSR)とは、証明書を発行するときに認証局に対してメッセージのことです。
CSRの発行は、手持ちのMacのキーチェーンアクセスアプリから行います。
キーチェーンアクセスから証明書アシスタント→認証局に証明書を要求を選択します。
上記の画面が表示されるので、メールアドレスと通称部分に任意の内容を記入します。
要求の処理では、ディスクに保存と鍵ペア情報を指定にチェックを入れます。
入力とチェックを入れたら続けるを選択します。
CertificateSigningRequest.certSigningRequestの保存先の選択が求められるので、任意のディレクトリに保存しましょう。
鍵ペア情報の選択が求められますが、こちらは初期値のままで大丈夫です。
完了すると上記画面が表示されます。
以上でCSRの発行は完了です。2. 証明書の生成
認証キー同様に、Apple Developer Programのページにアクセスし、左側の「Program Resources」以下に表示されている「Certtificates, Identifiers & Profiles」をクリックします。
次に「Certificates」にアクセスし、Certificates横のプラスボタンをクリックします。
Create a New Certificate画面が開きますので、Servicesの項目配下にあるApple Push Notification service SSLにチェックを入れます。
証明書にはSandbox環境だけへの接続を認可するものとSandboxとProduction環境どちらへの接続を認可するものがあります。
後者のキーだけでも開発用APNsキーと本番用APNsキーを兼用することはできますが、基本的にはSandbox用とProduction用は別々に発行したほうが良いと思います。
Platformと通知を利用するアプリのAppIDの選択が求められるので、通知を利用するAppIDを探し選択し次に進みます。
先程発行したCSRファイルの登録が求められるので、Choose Fileから発行したCSRファイルを選択します。
次へ進むと証明書のダウンロードが可能になります。生成した証明書をダウンロードしましょう。
cer形式の証明書がダウンロードできます。3. p12形式に変換する
Firebase Cloud Messagingでは、cer形式の証明書のままではアップロードできませんので手元でp12形式で書き出す必要があります。
ダウンロードしたcer形式の証明書をダブルクリックし、キーチェーン.appに証明書を追加します。
追加した証明書上で右クリック→「キー名」を書き出すを選択します。
証明書の名称、保存先の入力選択が求められるので任意の内容に設定します。フォーマットは「個人情報交換(.p12)」のままで大丈夫です。
保存時にパスワード設定が求められます。任意のパスワードを設定して書き出しましょう。
OKを押すとまれにキーチェーンアクセスからのパスワード入力を求められることがありますが、Macのログインパスワードを入力すればOKです。
任意のパスにp12形式の証明書が書き出されていれば成功です。Firebaseに認証キー・証明書をアップロードする
Firebaseのコンソールを開き、Push配信を行うプロジェクトを選択します。
選択後、プロジェクトの概要横に表示されている歯車アイコンをクリックし、設定ページを表示します。
次にクラウドメッセージングの設定項目を選択し、iOSアプリの設定から証明書をアップロードすることができます。
Push通知を送るには、認証キー・証明書どちらかをアップロードすればFCMからAPNsを通して、アプリがインストールされている端末に対して通知を送ることが可能になります。重ねてにはなりますが証明書を用いる場合、開発環境用と本番環境用それぞれの証明書をアップロードする必要があります。
片方だけアップロードしてもどちらかの環境では通知が届かないということが発生しますので注意が必要です。おわりに
FCMでは、現在認証キーを用いることが推奨されていますが未だに証明書形式でのFCMの利用方法などが記載されている事例が見られたので、改めて2つの方法があるよーということを書かせていただきました。
駆け足にはなりましたが参考になりますと幸いです。