- 投稿日:2019-12-05T22:49:29+09:00
【Firebase Analytics】Universal Links/App Linksでアプリを起動した回数が知りたい
はじめに
- この記事はオールアバウトアドベントカレンダー6日目の記事です。
- これまでモバイルアプリ(Android, iOS)の集計にGoogleアナリティクスSDKを利用していたのですが、2019年10月にGoogleアナリティクス開発者サービスSDKが終了 しました(Googleアナリティクス 360 のユーザーのプロパティには影響はありません)
- 今後のモバイルアプリ向けの集計は、Firebase SDKを利用して集計するFirebase Analyticsに移行する必要があります
- それに伴いFirebase Analyticsに移行したのですが、Universal Links/App Linksでモバイルアプリを起動したときのイベント数を分析する方法がわからなかったので、調べた結果を共有します
※仕様が変更される場合もあるので、必ず公式のマニュアルを参照しましょう!
導入:Universal Links/App Linksとは
本記事ではあまり詳細に説明しませんが、ざっくり説明すると、Webとアプリ(iOS,Android)に対応しているサービスにおいて、Webの特定のURLにアクセスすると、下記のような動作をする仕組みのことです。
- アプリがインストールされている場合→アプリが起動する
- アプリがインストールされていない場合→Webページが開かれる
iOSではUniversal Links, AndroidではApp Linksと呼ばれています。
実装方法などは公式のドキュメントをご覧ください。
Universal Links - Apple Developer
Android アプリリンクの処理 | Android Developers結論:Universal Links/App Linksでアプリを起動したとき、firebase_campaignというイベントが自動計測される
いきなり結論ですが、Firebase Analyticsにおいて、Universal Links/App Linksでモバイルアプリを起動したときのイベントは、firebase_campaign というイベント名で計測されます。
実は公式のドキュメントをちゃんと読むとしっかり書いてあります。
ユニバーサル リンクは、utm パラメータ(utm_source、utm_medium、utm_campaign)を含めるように設定することができます。関連データは [アトリビューション] レポートに表示されます。
ユニバーサル リンクの例:
http://my.app.link?utm_campaign=myappcampaign&utm_source=google&utm_medium=cpc
ユーザーがリンクをクリックすると、firebase_campaign イベントが上記のパラメータとともに記録され、キャンペーン関連のイベントについては次のデータが表示されます。
キャンペーン: myappcampaign
参照元: google
メディア: cpc
https://support.google.com/firebase/answer/6317518?hl=ja上記はUniversal Linksについてですが、App Linksについても同様のイベント名で集計されています。
Android App Links は、utm パラメータ(utm_source、utm_medium、utm_campaign)を含めるように設定することができます。関連データは [アトリビューション] レポートに表示されます。
Android App Link の例
http://example.com/gizmos?utm_campaign=myappcampaign&utm_source=google&utm_medium=cpc
ユーザーがリンクをクリックすると、firebase_campaign イベントが上記のパラメータとともに記録され、キャンペーン関連のイベントについては次のデータが表示されます。
キャンペーン: myappcampaign
参照元: google
メディア: cpcこのfirebase_campaignイベントは、Firebase Analytics側で自動的に集計されるイベントのひとつのようなので、Firebase Analyticsが導入されていればモバイルアプリ側で特別になにか実装をしなければならないということはありません。
ちなみにfirebase_campaignイベントで自動で記録されるパラメーターは
source、medium、campaign、term、content、gclid、aclid、cp1、anid
だそうです。
https://support.google.com/firebase/answer/7061705?hl=jafirebase_campaignイベントの内容はどこから確認できるのか?
それでは、このイベントによって集計された値はどこから確認できるのでしょうか? ドキュメントには
関連データは [アトリビューション] レポートに表示されます。
と記載されています。Eventsレポートからは確認できない
サイドバーにある[Events]をクリックすると集計された各イベントの回数やパラメータ等を確認できるのですが、ここにはfirebase_campaignという名前のイベントは表示されていませんでした。
Conversionsレポートでコンバージョンイベントに紐づく参照元として確認できた
サイドバーにある[Conversions]をクリックすると、コンバージョンイベントのレポート結果を閲覧できます。
例えばデフォルトでコンバージョンイベントとして用意されている first_open(アプリ初回起動)イベントを選択すると、画面下部あたりにそのコンバージョンイベントの参照元が表示されています。
この「参照元」パラメータが、firebase_campaignイベントで集計されたutm_source
の値だと思われます(違っていたら申し訳ありません…)その他のFirebaseコンソール画面を調べてみたのですが、これ以外の画面でfirebase_campaignイベントの計測結果を確認できる箇所が見つかりませんでした
![]()
この画面が
[アトリビューション] レポート
なのでしょうか?どの画面が該当の画面なのか、筆者にはわかりませんでした…。もしご存じの方がいれば、コメント等で教えていただけると大変喜びます![]()
しかし、この画面からは純粋な「モバイルアプリを起動した回数」を特定することができません…。
BigQuery経由でfirebase_campaignイベントを集計してみた
いろいろ調べてみたのですが、Firebaseコンソール画面上でfirebase_campaignイベントの内容を確認する方法がわかりませんでした
![]()
ですが、BigQueryからFirebase Analyticsの生データを分析できるので、そこから結果を確認することができました。Firebase Analyticsで集計されたイベントの生データは、BigQuery経由でアクセスすることができます。
具体的な連携方法等は公式のドキュメントや他の方のQiitaの記事で確認していただければと思います。
Firebase の BigQuery Export - Firebase ヘルプBigQueryで、前日のUniversal Links/App Links経由でモバイルアプリを起動したイベント回数を、プラットフォーム(iOS, Android)と参照元等の組み合わせで表示するクエリは下記のようになりました。
ぜひみなさんのプロジェクトでも参考にしてください
SELECT platform, (SELECT value.string_value FROM UNNEST(event_params) AS x WHERE x.key = "source") AS app_source, (SELECT value.string_value FROM UNNEST(event_params) AS x WHERE x.key = "medium") AS medium, (SELECT value.string_value FROM UNNEST(event_params) AS x WHERE x.key = "campaign") AS campaign, COUNT(DISTINCT user_pseudo_id) as users, COUNT(user_pseudo_id) as eventCount FROM `{your_project}.analytics_{your_id}.events_*`, WHERE PARSE_DATE("%Y%m%d", event_date) = DATE(DATETIME_ADD(CURRENT_DATETIME, INTERVAL -1 DAY)) AND _TABLE_SUFFIX = FORMAT_DATE("%Y%m%d", DATE(DATETIME_ADD(CURRENT_DATETIME, INTERVAL -1 DAY))) AND event_name = 'firebase_campaign' GROUP BY platform,app_source,medium,campaign ORDER BY users desc所感:独自のイベントを実装するのもアリかも?
![]()
- 本記事はFirebase Analyticsをモバイルアプリに導入した際に、Universal Links/App Linksでアプリを起動したときの自動計測イベント
firebase_campaign
の分析方法について紹介しました- firebase_campaignイベントはFirebaseコンソール画面からだと確認しづらい?かも?
- それならばいっそ独自のカスタムイベントとして、Universal Links/App Linksでアプリを起動したときにFirebase Analyticsの集計イベントを発火させるように実装したほうが、後々集計しやすいのではないかと思いました
- 投稿日:2019-12-05T22:19:04+09:00
Xcode11のSpell Checkにつまずいた
はじめに
Xcode11 でとうとう Spell Check が導入されました
使い方
とりあえずスペルチェックを常にONにします。
Edit -> Format -> Spelling and Grammar -> Check Spelling While Typing
これで下記のようにスペルミスの部分に赤い点線が表示されます。
ショートカット
- command + ;
スペルミスの箇所にフォーカスしてくれます。- command + :
下記のように Spelling and Grammar のウィンドウを開いてくれます。
ここの下に候補が出てるので選択して Change をクリックすると書き換えてくれます![]()
メニュー
赤点線部分を選択し右クリックすると下記のように変換候補と Ignore Spelling, Learn Spelling が表示されます。
その他
Macのスペルチェック
関係ないですが Mac はシステム環境設定 -> キーボード -> ユーザ辞書 -> 英字入力中にスペルを自動変換にチェックを入れているとスペルミスしていても勝手に変換してくれたりします(たまに効かないですが
)
UITextChecker
こちらも関係ないですが UIKit には
UITextChecker
というスペルチェックをしてくれるクラスがあります。(iOS3.2 からあるらしいけど初めて知った)
使い方は下記に詳しく書いてありました。
これも試してみた結果、精度は Xcode のスペルチェックの方が高そうでした。
Ignore&Learn
下記のように
csvText
のcsv
に赤線がある場合、無視するようにしたい。
※ 以下の確認環境は Xcode11.0ですこういう時に Ignore Spelling 使うんやな!と思いやってみる。消えた!が、別のファイルを確認すると赤い...もう一度同じファイルに戻ってみると再び赤くなっている
よくわからんけど Learn Spelling やってみる。消えた!別のファイルを確認しても赤線は消えている
試しに Xcode を再起動してみる赤線は表示されない他のプロジェクトを開いてみる赤線は表示されない
ん?これもう一度
csv
に赤線を表示させたい場合どうするんだろ?ちょっと調べてみましたが、わかりませんでした...
さいごに
ついに Xcode でスペルチェックが導入されたのでスペルミスは撲滅されるのではないでしょうか
Ignore と Learn に関しては動作がよくわかりませんでしたがどっかのファイルに保存されてるはず??
どなたか詳しい方教えてください
(今回のはつまずいた記事ではないかも...)
- 投稿日:2019-12-05T22:08:42+09:00
ARKitについて まとめ
はじめに
こんにちは!
ARアドベントカレンダー1日目です。(いきなり忘れてました。すみません。。。)
カレンダーのいくつか日数が空いていたので、ARKitについては分けて記載していこうと思います。まず今回は、ARKitの概要についてまとめます。
ARCoreとの比較や技術調査、これからARKitをはじめてみようと思う人向けの記事です。
とりあえず試してみたいと言う方は こちらの記事(ARKitのはじめかた その1)をどうぞ。ARKitとは
・Apple社が提供するAppleデバイス用のAR開発ツール
・カメラ越しの画面で、現実に即した3D表現可能にする
・2017年にARKit(ARKit1)が発表され、2019年現在はARKit3がリリースこれまでのバージョンと機能は、
Version 機能 2017.09 ARKit(ARKit 1.0) ポジショントラッキング, 平面検出(床), ポイントクラウド(3次元の点座標情報)取得, 周囲の明るさ推定, 現実空間への当たり判定(HitTest), 現実空間とのスケール一致, 顔認識 2018.01 ARKit 1.5 マーカー検出, マーカートラッキング(追跡), 平面検出(垂直) 2018.09 ARKit 2.0 マーカートラッキング(2D複数・3D), 取得した情報の再現(エクスポート/インポート), 現実空間の位置との関連付け, 即時ユーザー共有, 顔認識(視線・舌) 2019.09 ARKit 3.0 モーションキャプチャ, ピープルオクルージョン, 両面同時AR利用, ARKit2までの機能向上, (RealityKit・Reality Composer) バージョンが上がることに、より多くの現実の情報を判断出来るようになっており、ARKit2からは他端末との共有や再現が出来るようになりました。(AR=拡張現実と言う意味では、多くの人と認識出来るものこそ現実と言えるので重要なアップデートでした。)
また、今年発表されたARKit3では人の判定が出来るようになってきたので、より現実に合わせた表現が可能になっています。ただし、基本的には現実の何かの情報を検知する為のツールなので、
検知した後の表現には SceneKit, SpriteKit, MetalKitなど 別のツールとの組み合わせが必要です。
※2019発表のRealityKitは表現にも一部対応しています。仕組み(検知する方法)
ARKitが "どのように"動いているか
カメラを使った視覚型ARでは「カメラに映ったものを判定すること」と、
「現実の位置とカメラ上の仮想オブジェクトを適合して表現すること」が重要ですが、
画像の解析だけではなく 端末位置との差分を割り出し、画面に反映させ続けるには高い計算機能が必要となります。
※ARKitは、60FPSでカメラからの画像を取得し処理しています。ARKitでは これを実現する為に、SLAM(a)とVIO(b)を複合した「vSLAM」という技術が利用されています。
SLAM(Simultaneous Localization and Mapping)と VIO(visual inertial odometry)については、去年のk-boyさんの記事がとてもわかりやすいです。
Wikitudeや6d.aiといったSLAMベースのARツールは他にもあるのですが、
vSLAMを利用することで、比較的古めの端末(iPhone6s等)でも高品質なARを利用出来ています。
※ARKit発表時iPhone6sはまだ新しめの端末でしたが6D.AIを使ってる時の画面はこんな感じです。
SLAM(ジャイロセンサー無し)なのでバスでも位置がズレません。トラッキングモード
では、"何を検知するか"
ARKitが現実の情報を検知する為に、ARConfigurationという設定をします。
現在のトラッキングモードは、全7種類です。
ARConfiguration モードの説明 ARWorldTrackingConfigurationとの併用 ARWorldTrackingConfiguration 最も汎用的なモードです。ARKitがデバイスのバックカメラを使用して検出および追跡する可能性のある空間、人物、または既知の画像やオブジェクトに対するデバイスの位置と方向を追跡します。 - ARBodyTrackingConfiguration 人の動きを取得するのに相応しいモードです。デバイスの背面カメラを使用して、人物、平面、画像を追跡できます。 × AROrientationTrackingConfiguration バックカメラを使用してデバイスの向きのみを追跡します。 × ARImageTrackingConfiguration 動く画像の場所を取得し続けるのに相応しいモードです。デバイスのバックカメラを使用して既知の画像のみを追跡します。 × ARFaceTrackingConfiguration 顔の表情を取得するのに相応しいモードです。動きや表情など、デバイスのセルフィーカメラの顔のみを追跡します。 △ ARObjectScanningConfiguration 開発者用です。オブジェクト検知の準備に利用します。バックカメラを使用して、後で実行時にアプリに認識させる特定のオブジェクトに関する忠実度の高いデータを収集します。 × ARPositionalTrackingConfiguration 3D空間でのデバイスの位置のみを追跡します。 × 恐らく処理の問題から多くの機能が併用出来ませんが、
ARFaceTrackingConfigurationだけはARkit3より同時利用が可能になりました。
ただし、「両カメラの画像を同時に表示することは出来ない」「depthデータの取得は出来ない」という制限があります。
将来的に端末スペックが良くなれば多くの機能が併用可能になるでしょう。対応端末と性能
利用出来る端末には制限があります。
以下の端末は最新バージョンのARKit3(iOS13)を利用出来ます。
端末 chip(CPU) World Tracking Face Tracking Body Tracking People Occlusion iPhone 6s/6s Plus A9 ◯ - - - iPhone SE A9 ◯ - - - iPhone 7/7 Plus A10 ◯ - - - iPhone 8/8 Plus A10 ◯ - - - iPhone X A11 ◯ ◯ - - iPhone XS/XS Max A12 ◯ ◯ ◯ ◯ iPhone XR A12 ◯ ◯ ◯ ◯ iPhone 11 A13 ◯ ◯ ◯ ◯ iPhone 11 Pro/Pro Max A13 ◯ ◯ ◯ ◯ Face Trackingにはインカメラ(trueDepthCamera)が必要な為、iPhone X以上の端末。
高い計算量が必要な Body Tracking / People Occlusion には、A12以上のchipを搭載したiPhone XS以上が必要です。
また、トラッキングモードに変化はありませんが、iPhoneXRなど、単眼バックカメラの端末は視差によるDepthの取得が出来ません。よって深度を利用した環境取得は難しいです。(フロントでは取得出来ます。)まとめ
ARKitについての振り返りでした。
発表から2年しか経っていないわりにアップデートが多いのですが、
どんどん簡単になってるので始めるハードルは低くなりました。
次回は、ARKitの各バージョンで変化してきた実装方法について書こうと思います。以上です。ここまで読んで頂きありがとう御座いました!
次のアドベントカレンダーは、@KoukiNAGATA さんです。参考・関連記事
- 投稿日:2019-12-05T22:08:42+09:00
ARKitとは何か (ARKit1〜3のまとめ)
はじめに
こんにちは!
ARアドベントカレンダー1日目です。(いきなり忘れてました。すみません。。。)
カレンダーのいくつか日数が空いていたので、ARKitについては分けて記載していこうと思います。まず今回は、ARKitの概要についてまとめます。
ARCoreとの比較や技術調査、これからARKitをはじめてみようと思う人向けの記事です。
とりあえず試してみたいと言う方は こちらの記事(ARKitのはじめかた その1)をどうぞ。ARKitとは
今更ですが ARKitとは
・Apple社が提供するAppleデバイス用のAR開発ツール
・カメラ越しの画面で、現実に即した3D表現可能にする
・2017年にARKit(ARKit1)が発表され、2019年現在はARKit3がリリース です。これまでのバージョンと機能は、
Version 機能 2017.09 ARKit(ARKit 1.0) ポジショントラッキング, 平面検出(床), ポイントクラウド(3次元の点座標情報)取得, 周囲の明るさ推定, 現実空間への当たり判定(HitTest), 現実空間とのスケール一致, 顔認識 2018.01 ARKit 1.5 マーカー検出, マーカートラッキング(追跡), 平面検出(垂直) 2018.09 ARKit 2.0 マーカートラッキング(2D複数・3D), 取得した情報の再現(エクスポート/インポート), 現実空間の位置との関連付け, 即時ユーザー共有, 顔認識(視線・舌) 2019.09 ARKit 3.0 モーションキャプチャ, ピープルオクルージョン, 両面同時AR利用, ARKit2までの機能向上, (RealityKit・Reality Composer) バージョンが上がることに より多くの現実の情報を判断出来るようになっており、ARKit2から他端末との共有や再現が出来るようになりました。
また、今年発表されたARKit3では人の判定が出来るようになってきたので、より現実に合わせた表現が可能になっています。
AR=拡張現実と言う意味では多くの人と認識出来るものこそ現実と言えるので、重要なアップデートが毎年続いています。ただし、基本的には現実の何かの情報を検知する為のツールなので、
検知した後の表現には SceneKit, SpriteKit, MetalKitなど 別のツールとの組み合わせが必要です。
※2019発表のRealityKitは表現にも一部対応しています。
仕組み(検知する方法)
ARKitが "どのように"動いているか
カメラを使った視覚型ARでは「カメラに映ったものを判定すること」と、
「現実の位置とカメラ上の仮想オブジェクトを適合して表現すること」が重要ですが、
画像の解析だけではなく 端末位置との差分を割り出し、画面に反映させ続けるには高い計算機能が必要となります。
※ARKitは、60FPSでカメラからの画像を取得し処理しています。
ARKitでは これを実現する為に、SLAM(a)とVIO(b)を複合した「vSLAM」という技術が利用されています。
SLAM(Simultaneous Localization and Mapping)と VIO(visual inertial odometry)については、去年のk-boyさんの記事がとてもわかりやすいです。
Wikitudeや6d.aiといったSLAMベースのARツールは他にもあるのですが、
vSLAMを利用することで、比較的古めの端末(2015発売のiPhone6s等)でも高品質なARを利用出来ています。因みに6D.AIを使ってる時の画面はこんな感じです。
SLAM(ジャイロセンサー無し)なので 移動中のバスでも位置がズレませんでした。トラッキングモード
では、"何を検知するか"
ARKitでは現実の情報を検知する為に、ARConfigurationという設定をします。
現在の設定は、全7種類です。
ARConfiguration 設定の説明 備考 ARWorldTrackingConfiguration 最も汎用的なモードです。ARKitがデバイスのバックカメラを使用して検出および追跡する可能性のある空間、人物、または既知の画像やオブジェクトに対するデバイスの位置と方向を追跡します。 空間検知出来ます。 ARBodyTrackingConfiguration 人の動きを取得するのに相応しいモードです。デバイスの背面カメラを使用して、人物、平面、画像を追跡できます。 2Dと3Dのパターンがありますが、2Dの場合はARWorldTrackingConfigurationと併用出来ます。 AROrientationTrackingConfiguration バックカメラを使用してデバイスの向きのみを追跡します。 デバイスの向きはARWorldTrackingConfigurationでも適宜取得可能です。 ARImageTrackingConfiguration 動く画像の場所を取得し続けるのに相応しいモードです。デバイスのバックカメラを使用して既知の画像のみを追跡します。 理論値以下ですが、同時追跡は4,5枚が限界かもといった所感です。 ARFaceTrackingConfiguration 顔の表情を取得するのに相応しいモードです。動きや表情など、デバイスのセルフィーカメラの顔のみを追跡します。 ARWorldTrackingConfigurationとの併用可能です。 ARObjectScanningConfiguration 開発者用です。オブジェクト検知の準備に利用します。バックカメラを使用して、後で実行時にアプリに認識させる特定のオブジェクトに関する忠実度の高いデータを収集します。 精度は微妙です。 ARPositionalTrackingConfiguration 3D空間でのデバイスの位置のみを追跡します。 デバイスの位置はARWorldTrackingConfigurationでも適宜取得可能です。 ARFaceTrackingConfigurationはARKit3より同時利用が可能になりましたが、「両カメラの画像を同時に表示することは出来ない」「depthデータの取得は出来ない」という制限があります。
将来的に端末スペックが良くなれば多くの機能が併用可能になるでしょう。対応端末と性能
利用出来る端末には制限があります。
以下の端末は最新バージョンのARKit3(iOS13)を利用出来ます。
端末 chip(CPU) World Tracking Face Tracking Body Tracking People Occlusion iPhone 6s/6s Plus A9 ◯ - - - iPhone SE A9 ◯ - - - iPhone 7/7 Plus A10 ◯ - - - iPhone 8/8 Plus A11 ◯ - - - iPhone X A11 ◯ ◯ - - iPhone XS/XS Max A12 ◯ ◯ ◯ ◯ iPhone XR A12 ◯ ◯ ◯ ◯ iPhone 11 A13 ◯ ◯ ◯ ◯ iPhone 11 Pro/Pro Max A13 ◯ ◯ ◯ ◯ Face Trackingにはインカメラ(trueDepthCamera)が必要な為、iPhone X以上の端末。
高い計算量が必要な Body Tracking / People Occlusion には、A12以上のchipを搭載したiPhone XS以上が必要です。
また、トラッキングモードに変化はありませんが、iPhoneXRなど、単眼バックカメラの端末は視差によるDepthの取得が出来ません。よって深度を利用した環境取得は難しいです。(フロントでは取得出来ます。)まとめ
ARKitについての振り返りでした。
発表から2年しか経っていないわりにアップデートが多いのですが、
どんどん簡単になってるので始めるハードルは低くなりました。
次回は、ARKitの各バージョンで変化してきた実装方法について書こうと思います。以上です。ここまで読んで頂きありがとう御座いました!
次のアドベントカレンダーは、@KoukiNAGATA さんです。参考・関連記事
- 投稿日:2019-12-05T20:58:36+09:00
iOS13のセルのclipsToBoundsにつまずいた
はじめに
iOS13 にアップデートすると下記のような画面に遭遇。
しんぐるとんが見切れてる
対応
Accessory
にDisclosure Indicator
に設定しているのでcontentView
の範囲は下の青い範囲。とりあえずclipsToBounds
があやしい。デフォルトでセルの
contentView
のclipsToBounds
がtrue
になってるのでfalse
に設定。いけた
調査
とりあえず表示できましたが一応調査。
iOS13 以上でなる模様。
iOS12.4.1 iOS13.0 そもそもなんで iOS13 未満は
clipsToBounds
がtrue
なのに表示されるんだ??調べてみると下記がヒットした。(公式はみつからなかった...)
詳しくはわかりませんが iOS7.1 の頃から
UITableViewCell
のcontentView
はclipsToBounds
がtrue
に設定されていてもfalse
になるらしい。検証
iOS12.4.1 と iOS13 で
UITableViewCell
のcontentView.clipsToBounds
がどうなってるか調べてみました。実装はこんな感じ
ViewController.swiftoverride func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TestTableViewCell return cell }TestTableViewCell.swiftoverride func awakeFromNib() { super.awakeFromNib() print("awakeFromNib clipsToBounds: \(contentView.clipsToBounds)") } override func layoutSubviews() { print("layoutSubviews clipsToBounds: \(contentView.clipsToBounds)") }iOS12.4.1 の結果
awakeFromNib clipsToBounds: true layoutSubviews clipsToBounds: falseiOS13 の結果
awakeFromNib clipsToBounds: true layoutSubviews clipsToBounds: true layoutSubviews clipsToBounds: trueなんか2回呼ばれたけどとりあえず
clipsToBounds
がtrue
のままになってるさいごに
clipsToBounds
をfalse
にすることで対応しましたが、これでいいのかは不明...今のとこ iOS12 でも iOS13 でもおかしな表示にはなっていません。そもそも
View
からはみ出したレイアウトってどうなんだっていうのはありますが
- 投稿日:2019-12-05T20:20:55+09:00
StoryboardReferenceを用いて実装したTabBarControllerをカスタマイズする時の注意点
状況
Youtubeチャンネルの素材用にTinderの模擬アプリを作成する際、
1つのStoryboard
でTabBarController
と遷移先のVCを置く場合
カスタマイズは
Storyboard
上でそのままやってしまうTabBarController
かTabBar
のcustomclassを作って紐づけるのどちらかだと思うのですが
StoryboardReference
を用いてTabBarController
を実装した際
これだとうまくいかなかったのでメモしておきます参考
1つの
Storyboard
でTabBarController
と遷移先のVCを置く場合の参考文献です
・tabBarControllerとUINavigationControllerを同時に使いたい! - Takahiro Octopress Blog
・NavigationController とTabBarControllerを一緒に使う方法 - marikoootaの日記
・【Swift4】StoryboardでUINavigationControllerやUITabBarControllerを設定する方法【Xcode9】 | ニートに憧れるプログラム日記
・iOSアプリ開発入門#3 ~UITabBarController~ - Qiita対策
StoryboardReference
を用いてTabBarController
を実装した場合tabBarをカスタマイズ
TabBarController
のTabBar
をStoryboard
で直接編集する
->TabBar
表示されない
Storyboard Reference
で紐づいている遷移先のVCでカスタマイズしてみる
->tabBarController, tabBarのカスタムクラス作ってtabController.storyboard
のそれに紐づければおそらくいける
->VCとtabbarのstoryboardが別だから見た目変化なし
tabBarItem
でcustomclass作る
->tagで仕分けて実装したらカスタマイズできた
->見た目に関しては各VCで設置したtabBarItemしか反応しないのかもimport UIKit class StyledTabBarItem: UITabBarItem { //storyboardで設置した時 required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! setAppearance() } private func setAppearance() { //ライフサイクル内ならselfでVCから参照すれば使わなくてもいける->NCのみ //tabBarの画像は30*30px(Retinaだと60*60) switch self.tag { case 0: self.setTabBarItem(title: "", image: UIImage(named: "girlTabIcon")!, selectedImage: UIImage(named: "girlTabIcon-selected")!) case 1: self.setTabBarItem(title: "", image: UIImage(named: "emperorTabIcon")!, selectedImage: UIImage(named: "emperorTabIcon-selected")!) default: break } } func setTabBarItem(title: String, image: UIImage, selectedImage: UIImage) -> Void { self.title = title self.image = image//.withRenderingMode(.alwaysOriginal)<-PDFの時はこの設定いらない self.selectedImage = selectedImage//.withRenderingMode(.alwaysOriginal)<-PDFの時はこの設定いらない } }アイコン画像の設定
- KeynoteとかAdobeXDで適当にアイコン用に正方形の画像(300*300pxとか)作って設定したらむちゃくちゃはみ出した
->30*30pxの画像を用意したらできた
->アイコン画像にPDFを使うと
single scale
かつPreserve Vector data
にチェックで
30*30pxのデータのみでretina(60*60px)
にも対応できる
->しかしこれだとtabBar.tintColor = UIColor.pink
,tabBar.unselectedItemTintColor = UIColor.gray
などが使えないので
選択時と非選択時のデータを両方用意する必要がある
->後述のwithRenderingMode(.alwaysOriginal)
を使えば選択時の画像のみで良い
- アイコン画像のカスタマイズ時の注意点(PDFとPNGの違い)
->PDFだと用意するサイズは1種類でいい(↑の設定でretinaサイズにも対応できる)
->withRenderingMode(.alwaysOriginal)
を使えばPDFでも選択時の画像のみで良い//PNGのみ tabBar.tintColor = UIColor.pink tabBar.unselectedItemTintColor = UIColor.grayまたは
//PNGでもPDFでも使える tabBarItem.selectedImage = selectedImage.withRenderingMode(.alwaysOriginal) //↑これしないとtemplate扱いになって勝手に他の色に塗りつぶされる tabBar.image = image //↑非選択時で勝手にグレーになってもいいならこっち(あるいは.withRenderingMode(.alwaysTemplate))tabbarの上線消す方法
これに関しては
tabBarItem
からカスタマイズした影響でうまくやるやり方がわかりませんでした
->tabBar.shadowImage = UIImage
というやり方をしたいけど
tabBar
にアクセスしてカスタマイズする ortabBar(Controller)
からtabBarItem
をカスタマイズする
方法が分かっていないから別の方法
storyboardReference
側からtabBarItem
を編集する->
tabbBarControlller
やtabBar
の変更が反映されないのは
遷移先でtabbarItem
を新たに作っているせいかもしれない
->遷移先VCのtabBarItem
を消去 &storyboardReference
のtabBarItem
をcustom class
に準拠
->tabBar
が表示されない
tabBarItem
をNCではなくVCに移動する->VC側で
self.tabBarController.tabBar.shadowImage = UIImage()
->VCに移動しても影響はなかったが変更が反映されなかった参考
・【Swift4】Storyboardを分割してプロジェクトを作成する方法【iOS11】【Xcode9】 | ニートに憧れるプログラム日記
・【Xcode】Storyboard Referenceを活用してみた - Qiita・Storyboard Reference がいい感じ - SH Lab の アプリ開発部屋
->遷移先のVCをそれぞれのStoryboard
にする場合と遷移先のVCのみ1つのStoryboard
にまとめてしまう(TabBarController
は別)場合別のやり方が書いてある・UITabBarControllerをStoryboardで設定する時にTabBarItemが表示されない - Qiita
->本記事でも書いたようにTabBarController
と遷移先VCを分けた場合はTabBarItem
を遷移先VCにそれぞれ加えないとダメだと書いてある
・iOS Swiftチートシート・ライブラリまとめ UITabBar・UITabBarItem編 - Qiita
->tabBar.unselectedItemTintColor = UIColor.white
など
・iOS - UITabbarの中央アイコンをタブバーをはみ出したデザインにしたい|teratail
->HELargeCenterTabBarControllerの紹介
・【Swift5】UITabBarの色、背景画像、サイズやアイコン文字の色、フォント、サイズ、位置の調整まで網羅フルカスタマイズまとめ【Objective-C】 | ニートに憧れるプログラム日記
・UINavigationBar・UITabBar のカスタマイズ方法について - Qiita
->カスタマイズのTips.(appearance()
なくてもいけるからこれの存在意義はよくわからない。。。)
・UITabBarControllerの基本
->storyboardReference側からtabbarItemを編集するやり方が書いてある・[Swift]UIImageのレンダリングモードまとめ - Qiita(rederingModerでのやり方)
・[Swift] iOS で画像の色を動的に変える - Qiita(rederingModerでのやり方)
・[Swift] 画像の色を変更する - Qiita(tintColorでのやり方)
・[Swift] iOS で画像の色を動的に変える - Qiita(rederingModeでのやり方)
->今回のようにPDFではなくPNGでtabbarのアイコン作った場合は必要。
・【Swift】 アセット画像を3種類も書き出したくない - Qiita
->PDFでのやり方はこっち。
・【iOS】Tabbar メニューの画像のサイズや解像度に関して - 東京伊勢海老通信
->tabbarの画像サイズは30*30でないとダメ。
- 投稿日:2019-12-05T19:53:36+09:00
SwiftUI×HealthKitでiOSデータ入門(前編)
-準備 & 環境構築編-
Yahoo!Japan主催のハッカソン HackDay2019 出場のため,きゅうきょSwiftで iOS の HealthKit を触ることになったのでキャッチアップの内容を自分用にまとめていきます.ちなみにこの記事を書いている 2019/12/15 01:10 現在,リアルタイムで絶賛ハッカソン中です.自分が担当しているデータ周りだけをまとめていますが,焦っているため間違いなどあったらすみません
Xcode も Swift も HealthKit もはじめて触りますが,がんばって iPhone からデータを取得し,表示,データベースへ格納するところまで挑戦します.Swift のキャッチアップを兼ねているということもあり,少し長くなります.
- SwiftUI×HealthKitでiOSデータ入門(前編) -準備&環境構築編-
- SwiftUI×HealthKitでiOSデータ入門(中編) -HealthKitアクセス許可編-
- SwiftUI×HealthKitでiOSデータ入門(後編) -HelathKitデータ取得&Firebase編-
から構成されます.
開発環境
- macOS Catalina (10.15.1)
- ios (13.2.3)
- Xcode (11.2.1)
- Swift (5.1.2)
- Realm (4.1.1)
- RealmSwift (4.1.1)
環境構築
わかりやすい記事はたくさんあるので基本はググってもらうとして,ここでは自分がやったことをベースにまとめていきます.
Step1. ios と Xcode
-> Xcodeは最新の状態にし,Swift は Xcode のバージョンに紐づいているため別で必要なアップデートなどはなし.
Xcode でプロジェクトを作成しておいてください.ここではアプリ設定は SingleViewApp にしてUI設定は SwiftUI にしています.
※ Xcode (Swift)の Hello World に関しては SwiftUI Tutorials で行いました.
SwiftUI を触るのは初めてなのでキャッチアップとして SwiftUI Essentials を1通りやりました.ただしこのチュートリアルで作成するプロジェクトの完成形は,本記事のコンセプトから外れるので,ここでは チュートリアルの Section3 まで終えた状態 でスタートします.チュートリアルにしたがって ContentView.swift をいじった状態です.
ContentView.swift// // ContentView.swift // Project Name // // Created by My Name on 2019/12/06. // Copyright © 2019 Team Name. All rights reserved. // import SwiftUI // Main Process struct ContentView: View { // 垂直方向のプレビュー設定 var body: some View { VStack (alignment: .leading) { Text("Hello, World!") // 1つめのテキスト (VStack内のテキストは縦に並ぶ) .font(.largeTitle) // フォントを変更 .foregroundColor(.green) //カラーを変更 // 水平方向のプレビュー設定 HStack { Text("First App") // 2つめのテキスト .font(.subheadline) Text("-First Project-") // Hstack内のテキストは横に並ぶ .font(.subheadline) } } .padding() // 余白を取る } } // Mainのstructのインスタンスを生成し,画面に表示 struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }Canvas はこんな感じになっているはずです.
この状態から進めていきます.Step2. ios/mac のライブラリ管理ツール cocoapods をインストール
-> Realm をインストールするために必要.こちらの記事を参考にしました.
※ 当初は Realm を使う予定でしたが最終的に Realm ではなく Firebase を使っています.ただ,せっかくインストールまとめてあるので消さずに残すことにします.
sudo gem install cocoapodsStep3. Xcodeでプロジェクトを作成
これは初めに作成したものをそのまま使います.
Step4. Realm (レルム) のインストール
-> Realm はモバイル向けデータベースとして人気だそうです.まだ未定ですが,Realm を使うことを想定して進めます.解説はこちら.インストールはこちらのQiitaを参考にしました.
Step3で作成したプロジェクトのディレクトリでpod initを実行して Podfile を作成する.Realm公式のInstallationに CocoaPods でのインストール手順があるのでこれを参考にPodfile に必要な記述を追加.
pod installで必要なライブラリをインストール完了.
RealmSwiftの動作確認
上で入れたRealmの動作確認をします.Xcode でプロジェクトを立ち上げて contentView にコードを書いていきます.さっそく
import "RealmSwift"すると・・・
No such module "RealmSwift"のエラーが.こちらの記事を参考にすると Realm を利用する場合 Xcode のプロジェクトを
プロジェクト名.xcodeproj
ではなく,
プロジェクト名.xcworkspace
から開く必要があるとのこと.立ち上がりに時間がかかり,しばらくするとエラーなく起動.ためしにビルドしてみると・・・
今度は
Could not build Objective-C module 'RealmSwift'どうやら上のメニューから Product -> Scheme -> New Scheme を選択し,「RealmSwift」を選択しないといけないようです.
これで Run するとようやく
Build Succeeded
ここまでで環境設定周りは完了です.次回からは本格的に HealthKit からデータを取ってきて Realm に格納したいと思います.というわけで SwiftUI×HealthKitでiOSデータ入門 (中編) に続きます!
- 投稿日:2019-12-05T18:21:17+09:00
社員旅行アプリをドメイン駆動設計で開発してみた軌跡
こんにちは。ナビタイムジャパンの地図フレームワークエンジニアときどきAndroidエンジニア、3代目ゆうです。
これは NAVITIME JAPAN Advent Calendar 2019 9日目の記事です。
この記事ではナビタイム社員旅行用のしおりアプリをDDD(ドメイン駆動設計)で作ってみた軌跡についてお話したいと思います。
はじめに
ナビタイムジャパンでは毎年秋に社員旅行を開催しています。最近では社員旅行のある会社は珍しいみたいですが、ふだん仕事で関わることが殆どの同僚と非日常な雰囲気で交流できるのは中々に楽しく、私は気に入っています。
それになんといっても会社のお金で美味い飯、美味い酒、いい温泉である。しかして我らエンジニア、ただ消費するだけの旅行じゃ面白くない。せっかくならば社員旅行にかこつけて何か楽しい開発をしようじゃないかと、有志のアプリエンジニア数人が集まり勝手に始めた企画が今回お話する社員旅行アプリです。
旅のしおり(行程表)や各自の新幹線座席表、旅館の部屋番号などなど、必要な情報をまとめた一つのアプリを作っちゃおうという企画でした。社員旅行アプリの目的、要件
そんな些細なきっかけから始まった社員旅行アプリプロジェクトですが、大まかな目的として下記を設定しました。
- 開発の目的
- 部署を超えて沢山の人達とアプリを作りたい、交流したい
- 新しい技術を使いたい、挑戦したい
- 今後の業務で参考になる設計・実装のアプリにしたい
- アプリの要件
- 社員旅行での”困った”を解決するアプリにする
- Android/iOSのネイティブアプリにする
本記事では「今後の業務で参考になる設計・実装のアプリにしたい」という目的からDDDを採用した経緯および実践してみたドメインモデル化の過程について書いていきたいと思います。
実際はよりアプリ実装に近い設計(アーキテクチャ)の話や、それに合わせたフレームワークライブラリの選定なども同時に行いましたが、話が大きくなりすぎてしまうため割愛します。ドメイン駆動設計の採用
今回、有志メンバー同士で話し合い、以下の理由からDDDを導入することにしました。
チーム単位でのドメイン駆動設計を経験したい
一つ目の理由にして、最大の理由がこちらです。
今回の参加メンバーの習熟度としては、いわゆるDDD本1やIDDD本2の読了者から、「DDD?なにそれ?」な人まで幅広くいました(ちなみに、私は「DDD?なにそれ?」側の人間です)。中には個人のアプリ開発でDDDを実践しているという人や、普段の業務でドメインを意識した設計をしているという人もいましたが、チームとして導入した経験のある人はいませんでした。
新規サービスの立ち上げなどでもない限り、業務で突然導入するにはハードルが高いのが実情だと思います。しかし今回の社員旅行アプリは、「新規開発」で「規模もそこまで大きくなく」、「趣味半分」なので練習台として扱うには丁度いいのでは?という意見から実現することとなりました。
Android/iOSエンジニア間で用語の認識齟齬などを防ぎたい
前項のとおり、DDDの浸透が十分とは言えない開発現場において、身近な課題としてよく挙がるのがこちらです。
ナビタイムのアプリはその多くがAndroid/iOSの両OSについてネイティブアプリとして開発し、基本的には同じ機能を持つように作ります。
しかし、多くの場合それぞれ別の開発者が担当することになるため、機能の仕様検討までは一緒にやることが多くとも、コードレベルまで同調して開発ということはあまりやっていません。
その結果、同じ機能、同じ概念を指した言葉であってもコード上の名前が異なっているという事例は珍しくありませんでした。今回、社員旅行アプリはAndroid/iOSの両OSについてネイティブアプリとして開発するという要件がありました。
また、「沢山の人達で作りたい、交流の場にしたい」という想いから、社内に広くコントリビュータを募り、不特定多数の開発者で作ることも予定していました。その結果として懸念される前述のようなドメインモデルの認識齟齬を防ぐためにも、より厳密な形のDDDが効果的なのではという狙いも導入を後押ししました。ドメイン駆動設計の実践
DDDを実践していくにあたり、お手本としてDroidKaigi2019のカンファレンスアプリおよびその設計について解説したあんざいゆき氏の講演を参考にしました。
対象ドメインの確認
まず、ドメインモデルを考える上で、対象となるドメインそのものを決定しなくてはなりません。今回は特に大きな議論もなく、対象ドメインは以下の通りとして決定しました。
- 2019年度ナビタイム社員旅行
ユビキタス言語の洗い出し
例年、社員旅行が近づくと総務の方から旅のしおり(PDFファイル)や各個人の旅行コース、新幹線座席などが一覧になったスプレッドシートが展開されます3。そのため、まずはそれらの資料からひたすらにユビキタス言語となりうる単語についてざっくり書き出していくという作業をはじめました。この時点では、あいまいな表現の重複や実際にアプリとして使うかどうかといった事は一旦置いておいて、読み進めながら目についた単語を書き出していきました。
書き出した一例を以下に引用します。
部署 氏名 性別 往路乗車駅 往路新幹線座席番号 コース別観光 1日目バス号車 2日目バス号車 部屋割り ...
さて、一先ずの書き出しが終わったところで、次はそれらの単語をユビキタス言語として整理することとしました。
書き出した一覧から、あいまいな表現や意味合いが重複する単語を統一し、共通言語としての日本語とコードに落とし込むための英語の2言語で定義していくこととしました。ここまでで、ユビキタス言語として定義した一例を以下に引用します。
個人情報【user】 氏名【name】 性別【gender】 部署【division】 1日目【day1】 2日目【day2】 乗車新幹線【shinkansen】 バス【bus】 観光コース【tripCourse】 部屋番号【roomNumber】 ...
ドメインモデル化
必要な項目名、単語名がユビキタス言語として出揃ったため、いよいよドメインモデル化を行います。
まず、より大きなくくりとなる概念から、それを構成する要素が何か整理することとしました。
ここでは、「個人情報」を例にとって解説していきます。まず、「個人情報」には「氏名」、「性別」、「部署」が含まれるだろうという事が直感的に分かります。また、今回の対象ドメインは「2019年度ナビタイム社員旅行」であるため、その人が乗車する「新幹線」や「バス」、旅行中の「観光コース」や「部屋番号」も紐付いていいはずです。
実際には、この結論まで紆余曲折経つつ議論を行ったのですが、最終的に以下のような「個人情報」モデルが出来上がりました。
- 個人情報【user】
- 部署【division】
- 氏名【name】
- 性別【gender】
- 社員属性【employeeAttribute】
- 1日目【day1】
- 乗車新幹線【shinkansen】
- バス【bus】
- 配布ドリンク【drink】
- お茶【tea】
- ビール【beer】
- 観光コース【tripCourse】
- 2日目【day2】
- 乗車新幹線【shinkansen】
- バス【bus】
- 観光コース【tripCourse】
- 部屋番号【roomNumber】
さらに、ドメインモデルを考える上で重要な要素として、そのモデルが値オブジェクトなのかエンティティなのかを判断します。
改めて上記「個人情報」モデルを見返してみると、仮にこれらの値が一致していたとしても必ずしも同一の個人であるとは限らない事が分かります。逆に、「部署」や「観光コース」といった属性値が変わったとしても、その個人であることは変わらないため、「個人情報」は同一性によって区別されるもの、すなわちエンティティであると言えます。エンティティを表現するためには、同一性を一意に識別できるIDが必要になってきます。
IDを付与するといっても、実態として何を使うかは考える必要があります。今回は対象ドメインである「2019年度ナビタイム社員旅行」における「個人情報」を一意に識別するためには社用メールアドレスの@以前が利用できるため、その文字列を利用することとしました。同様に、以降も全ての概念についてモデル化を行っていきました。
ドメインモデル実装
最後に実装の話に少しだけ触れていきたいと思います。
前項までで洗い出せたドメインモデルを実際のコードに落とし込む作業となります。
なお、実際はAndroid/iOSそれぞれをkotlin/Swiftで実装したわけですが、大まかな考え方は共通な箇所となっているためAndroid(kotlin)を例にとって解説します4。まず、実際に出来上がった「個人情報」のモデルクラスを御覧ください。
User.ktdata class User( val id: UserId, val division: Division, val name: String, val gender: Gender, val employeeAttribute: EmployeeAttribute, val day1: Day1?, val day2: Day2?, val roomNumber: String? ) : Serializable inline class UserId(val value: String) : Serializable先程記載したドメインモデルそのままになっていることがわかると思います。
ドメインモデルは基本的にdata class
またはinline class
として定義し、選択肢が限られている場合(Gender
やDivision
など)はenum class
として定義する形としました。
また、クラス名やプロパティ名にはユビキタス言語を使うようにしました。実際には、ここからドメインモデルレイヤー以外のUIや各種レイヤーの設計、実装があるのですが、DDDの実践としてはここまでとしたいと思います。
大変だったところ、議論の白熱したところ
前項までのように実践してみて、難しさを感じたポイントや議論の白熱したところがありましたので、その一部を紹介させていただきます。
表記ゆれ多い問題
これはユビキタス言語の洗い出し過程で発生しました。
今回は主に旅のしおりを使って用語の抽出を行ったわけですが、それ自体はあくまでヒト向けの読み物であるため表記ゆれや曖昧な語が多数出現しました(e.g. 旅館, 宿舎, ホテル)。
こういった表記ゆれや曖昧な語を直感的にまとめてしまう事は簡単ですが、今回は一つ一つ愚直に「どういう意味合いを含むのか」「どちらがより適切か」といった事を話し合うようにしました。
思っていた以上に時間はかかってしまいましたが、その分メンバー間での用語に対する認識齟齬は殆ど無くなったため、やはり大事なフェーズだったと思います。UIとモデルの関係性
ドメインモデルを考える上で意外と大変だったことが「UIの事を考えがち」になってしまったことです。今回集まったメンバーは全員アプリエンジニアだったのですが、弊社のアプリ開発手法としては画面ベースのプロトタイピングから各画面の仕様や機能を考える事が多いため、ふだんの慣習からドメインモデルもUI基準になってしまうという問題がありました。
すなわち、「この画面でこの情報を一緒に出したいから、同じモデルに入るべきではないか?」といった発想です。ドメインモデルがUIの事を意識するべきでないという認識は持っていましたが、分かっていてもついつい画面ファーストに考えてしまうことは少なからずありました。そこで、今回は議論が行き詰まってきた場合には敢えていろんなパターンの画面を仮想的に考えて、その中で共通のモデルを使う場合にどこでも違和感がないようにすることで普遍的なドメインモデルを探るという方針を取りました。
結果として、実装フェーズに入ってから画面デザインの変更が起きてもドメインモデルに対する修正は発生せずに済んだ事もあったので有効な手段かもしれません。新幹線は物か概念か
今回の例の中で最もDDDらしく、最も難しかった議論がこちらです。
旅行の行き帰りに乗る新幹線に関する情報について、当初は物理的な新幹線の車両としてモデル化しようと試みました。つまり新幹線とは、複数の車両を持ち、車両の中には無数の座席を持ち、各座席に着座する個人が紐づくものであると考えたのです。
しかし、この考え方だと実際にデータクラスに落とし込んだ時に大量の座席プロパティを持つなどあまり現実的なデータ構造ではなくなってしまいます。さらに、その新幹線の停車駅や発着時刻といった情報はまた別のモデルとして切り出すことになり、必要以上に複雑になってしまう事が分かりました。その後の議論で、今回必要な新幹線の情報とは、結局のところ「個人がどの新幹線のどの座席に乗るのか、何時に出発し何時に到着するのか」だけだということに気付きました。最終的に、それらの情報を持った値オブジェクト「乗車新幹線」という概念としてモデル化することで解決しました。
ドメインモデルの決定には、対象とするドメインにおいてその用語がどのような文脈で語られているのかを考慮しないと、適切なモデリングにはならないという良い例になったと思います。まとめ
社員旅行アプリという題材を元にDDDを実践してみた軌跡についてお話させていただきました。
私自身、「DDD?なにそれ?」な人だったわけですが、いざ勉強しつつ実践してみて本記事が書ける程度の理解を得ることが出来ました。
言うまでもなく分かっていたつもりではありましたが、改めてちゃんと設計することの難しさと大切さに気付けるいい機会になりました。きっかけこそ軽い気持ちで始まった社員旅行アプリプロジェクトでしたが、最終的に新卒1年目も含む30名(!)近くものエンジニア、デザイナが関わってくれました。
それだけの人数で一つのプロダクトを作れたこと、さらにそこでドメイン駆動設計を体系的に実践出来た事は大きな意味を持つのでは無いかなと思っています!参考
- 投稿日:2019-12-05T18:12:32+09:00
Android / iOS アプリのテスト配布サービスあれこれ(DeployGate、Firebase App Distribution、Bitrise)
この記事は くふうカンパニー Advent Calendar 2019、5日目の記事です。
くふうグループの Da Vinci Studio では、さまざまな Android / iOS アプリの開発やサポートを行なっています。
そしてふだんから複数のサービスを使ってテスト配布しているので、それぞれのサービスについての感想と使用例を書きます。1. DeployGate
2. Firebase App Distribution
3. Bitrise(の配布サービス)1 と 2 は fastlane を使ってアップロードしています。3 は Bitrise のデプロイステップを使っています。
また、最後におまけで Bitrise で Firebase App Distribution についても書いています。DeployGate
感想
- 日本語で説明されていてわかりやすい
- テスター向けの説明も日本語でサポートしやすい
- iOS でデバイスを追加するとき、UDID 取得から登録、再配布までの手順が簡単(dg コマンドラインツール が便利)
- 同じ配布ページでアップデート版を続けて配布できる(テスターに見てほしい特定のバージョンを開発者が指定できる)
- アップロード先に複数の配布ページを指定できるので、開発者用とテスター用で出し分けられる1
- テスターがアプリをインストールしたときなどに通知を受信できる(サポートするときに便利)
- 料金はグループ開発向けプランだと最低でも 10,000円 / 月(税込)、それでも使っていきたい
使用例: fastlane を使うとこんな感じ
Fastfile でレーンを設定
build_app(...) deploygate( api_token: ENV["DEPLOYGATE_API_TOKEN"], user: ENV["DEPLOYGATE_USER"], ipa: "./davincistudio.ipa", message: "メッセージ", disable_notify: true )詳しくは、https://docs.deploygate.com/docs/fastlane をご覧ください。
ENV["DEPLOYGATE_API_TOKEN"]
とENV["DEPLOYGATE_USER"]
は .env ファイル に設定しています。Firebase App Distribution
https://firebase.google.com/docs/app-distribution?hl=ja
感想
- 日本語で説明されていてわかりやすい
- テスター向けの説明は英語だが、開発者がリリースノートを工夫すれば何とかなる
- Android / iOS アプリ開発ではたいてい Firebase を使うのでなるべく統一したい
- 今のところ 0 円だが料金はよくわかっていない
- まだベータ版なので今後に期待
使用例: fastlane を使うとこんな感じ
fastlane プラグインをインストール
$ fastlane add_plugin firebase_app_distributionFirebase CLI をインストール
$ curl -sL firebase.tools | bashFastfile でレーンを設定
build_app(...) firebase_app_distribution( app: ENV["FIREBASE_APP_ID"], release_notes: "リリースノートです。", groups: "dvs" # 作成したグループのエイリアス )詳しくは、
Android: https://firebase.google.com/docs/app-distribution/android/distribute-fastlane や
iOS: https://firebase.google.com/docs/app-distribution/ios/distribute-fastlane をご覧ください。
ENV["FIREBASE_APP_ID"]
は .env ファイル に設定しています。Bitrise(の配布サービス)
感想
- Bitrise で
Deploy to Bitrise.io
ステップを使うとそのまま配布準備ができるので簡単- 日本語に対応していないのでテスターにとってはわかりづらい
- 料金は Bitrise の利用料に含まれるので、Bitrise を使っていれば気にならない
使用例
Bitrise で
Deploy to Bitrise.io
ステップを設定し、テスターを Team に登録します。
(テスターに登録しなくても公開インストールページからの配布もできます。)詳しくは、
Android: https://devcenter.bitrise.io/jp/deploy/android-deploy/deploying-android-apps/ や
iOS: https://devcenter.bitrise.io/jp/deploy/ios-deploy/deploying-an-ios-app-to-bitrise-io/ をご覧ください。
また iOS デバイスの登録は、 https://devcenter.bitrise.io/jp/testing/registering-a-test-device/ をご覧ください。[おまけ] Bitrise で Firebase App Distribution
Firebase CLI をインストールするステップを追加
https://firebase.google.com/docs/cli?hl=ja#install-cli-mac-linux にて「CI/CD 環境での自動デプロイ」は自動インストールスクリプトが推奨されているので、そのとおり設定します。
$ curl -sL firebase.tools | bashBitrise ではこのスクリプトステップを追加するだけで実行できました。
+ curl -sL firebase.tools + bash -- Checking for existing firebase-tools on PATH... Your machine already has firebase-tools@]0;Firebase CLI7.8.1 installed. Nothing to do. -- All done![最後に] つまずきそうなところ共有
Error: App Distribution could not find your app x:xxx:xxxx:xxx. Make sure to onboard your app by pressing the "Get started" button on the App Distribution page in the Firebase console: https://console.firebase.google.com/project/_/appdistributionこれはたぶん Firebase コンソールで App Distribution を「開始」していません。Firebase コンソールの App Distribution ページに移動し、アプリを選択して「開始」しましょう。
そうでない場合は、アプリ ID が違うかもしれません。Settings 全般 のマイアプリで確認して合わせましょう。[!] Could not find action, lane or variable 'firebase_app_distribution'. Check out the documentation for more details: https://docs.fastlane.tools/actionsこれはたぶん fastlane の App Distribution プラグインを追加していません。
$ fastlane add_plugin firebase_app_distribution
を実行して追加しましょう。以上
- 投稿日:2019-12-05T17:48:23+09:00
iOS13のUISegmentedControlをiOS12風に実装する
環境
- Xcode11.2
- Swift5.1
はじめに
今回のXcode11の登場で知らないうちに変わっている、iOSの標準UIの中に
UISegmentedControl
があります。
世界ーのデザイン会社であるAppleのデザイナーとエンジニアが生み出した標準UIなので、1-2年以内にはこのUISegmentedControlがユーザにとっては標準になる事なのでしょう。しかし、新しいデザインとはそれなりに古い認識や先入観を破壊してくるもので
既にストアに出ているアプリのテイストと必ず合うとは限りません。
仕事の関係上、ユーザの反応をきちんと伺うまで、なかなかXcode10.xから離れられないiOS開発者の方も居ると思います。最初はUIの変更の程度なのでそこまで難しくないと考えていました。
しかし、普通にbackgroundColorやtintColorを指定するだけでは中々うまくいかず、すぐに終わるかと思いましたが結果2~3時間程の時間を要してしまいました。間違っている場合がある場合、遠慮なくコメントを頂けたらと思います。
iOS13(Xcode11)からのUISegmentedControlのデザイン
UIImageのextensionを追加
extension UIImage { convenience init?(color: UIColor, size: CGSize) { let rect = CGRect(origin: .zero, size: size) UIGraphicsBeginImageContextWithOptions(size, false, 1) color.setFill() UIRectFill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() guard let cgImage = image?.cgImage else { return nil } self.init(cgImage: cgImage) } }こちらこのままコピ&ペーストして頂いても動きます。
まずはUIImageを初期化する際に引数のcolorを指定してリサイズしてくれたUIImageを返してくれる拡張を追加します。convenience initializerについては以下の記事をそのまま引用すると
convenience initializerは他のイニシャライザに処理を委譲(delegate)することができます。
つまりイニシャライザの中で別のイニシャライザを呼ぶことが出来ます。[Swift] convenienceイニシャライザとdesignated(指定)イニシャライザ
UISegmentedControlのextensionを追加
extension UISegmentedControl { func iOS12Style() { if #available(iOS 13.0, *) { let unselectedTintImage = UIImage(color: .gray, size: CGSize(width: 1, height: 30)) let selectedTintImage = UIImage(color: .blue, size: CGSize(width: 1, height: 30)) let highlightImage = UIImage(color: UIColor.blue.withAlphaComponent(0.20), size: CGSize(width: 1, height: 30)) // 選択されていない時のUISegmentedControlの背景 setBackgroundImage(unselectedTintImage, for: .normal, barMetrics: .default) // 選択時のUISegmentedControlの背景 setBackgroundImage(selectedTintImage, for: .selected, barMetrics: .default) // 選択中のUISegmentedControlのハイライト時の背景 setBackgroundImage(highlightImage, for: .highlighted, barMetrics: .default) setTitleTextAttributes([.foregroundColor: UIColor.gray], for: .selected) setTitleTextAttributes([.foregroundColor: UIColor.blue], for: .normal) setDividerImage(selectedTintImage, forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default) } } }// 選択中のUISegmentedControlのハイライト時の背景
こちらの説明が少し分かりにくいと思いますが、iOS13からsegmentを切り替えるトリガーが、タップ時に加えてスクロールに選択中のsegmentが追従してくるような仕様に変わりました。
その際の背景色(と言うか画像)が指定できるので、iOS12の仕様に合わせて少し薄めの背景色を設定してあげました。(これがないと結構お粗末な感じになるのであった方が個人的にはあった方が良いと思います)結果 こんな感じになります
iOS12のデザインっぽくする事ができたのではないでしょうか??
ソースコード全体
import UIKit final class ViewController: UIViewController { @IBOutlet private weak var segment: UISegmentedControl! override func viewDidLoad() { super.viewDidLoad() segment.iOS12Style() } } extension UIImage { convenience init?(color: UIColor, size: CGSize) { let rect = CGRect(origin: .zero, size: size) UIGraphicsBeginImageContextWithOptions(size, false, 1) color.setFill() UIRectFill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() guard let cgImage = image?.cgImage else { return nil } self.init(cgImage: cgImage) } } extension UISegmentedControl { func iOS12Style() { if #available(iOS 13.0, *) { let unselectedTintImage = UIImage(color: .gray, size: CGSize(width: 1, height: 30)) let selectedTintImage = UIImage(color: .blue, size: CGSize(width: 1, height: 30)) let highlightImage = UIImage(color: UIColor.blue.withAlphaComponent(0.20), size: CGSize(width: 1, height: 30)) // 選択されていない時のUISegmentedControlの背景 setBackgroundImage(unselectedTintImage, for: .normal, barMetrics: .default) // 選択時のUISegmentedControlの背景 setBackgroundImage(selectedTintImage, for: .selected, barMetrics: .default) // 選択中のUISegmentedControlのハイライト時の背景 setBackgroundImage(highlightImage, for: .highlighted, barMetrics: .default) setTitleTextAttributes([.foregroundColor: UIColor.gray], for: .selected) setTitleTextAttributes([.foregroundColor: UIColor.blue], for: .normal) setDividerImage(selectedTintImage, forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default) } } }
- 投稿日:2019-12-05T08:29:16+09:00
RailsエンジニアがReactNativeエンジニアになるにあたって苦しんだところワースト3
はじめに
こんにちは、株式会社OKANで「おかんPay」というプロダクトのPdMをしながらも、自らバリバリReact Nativeで実装している かなすぎ と申します。
元々は、RailsでOKANの社内システムを構築していたサーバーサイドエンジニアだったのですが、志願してReact Nativeを使うアプリエンジニアになってから約半年が立ちました。
そこで、RailsエンジニアがReact Nativeを扱うアプリエンジニアになる時に苦しんだ箇所をまとめることで、自分と同じようにReact Nativeを新たに勉強する人の助けになればと思いながら記事を書きました。想定読者
- Railsエンジニアだが、これからReactNativeアプリもつくってみたいなって思っている人
- React.js, Vue.jsなどフロントエンドフレームワークをがっつり実装したことないけど、React Nativeでアプリ作ってみたい人
RailsエンジニアがReactNativeエンジニアになるにあたって苦しんだところワースト3
RailsではJavascriptも使うことはあったのですが、MVCでいうViewのDOM操作をjQueryで軽く行うくらいの経験値であったので、ES6でがっつりロジックを書くというのも覚えることが多くて大変でした。また、Vue.jsやReact.jsなどで用いられる状態管理や、React.js特有のライフサイクルメソッドやJSX。そして、ウェブ開発では存在しないアプリ特有のDeepLinkや画面遷移の方法なども大きく異なっているので、キャッチアップに苦労しました。
ワースト1位:ES6(ES2015)
React NativeはES6で書きます。Javascriptは、ES6以降とそれより前では使える機能は大きく変わりました。現在流行りのフロントエンドのフレームワークであるAngular.js、React.js、Vue.jsなどでも、基本的にはES6以降の書き方で書くことが多いです。JavascriptはViewの見た目を動的に変える軽量な言語という認識でしたが、ES6を勉強してからは、認識が大きく変わりました。
イマドキのJavaScriptの書き方2018 - Qiita
ES2015(ES6) 入門 - Qiitaアロー関数
ES6以前では、関数を定義する時は、functionを使用していましたが、ES6以降では、アロー関数を用います。記号ばっかりで最初は慣れないかと思いますが、慣れれば、短い記述で関数を定義できるので非常に便利です。()を省略できたり、様々な記述パターンがあるので、引数の数など場合によって使いわけましょう。
// 何もしない関数 () => {} // 引数が一つだったら「()」不要 value => {} // ブロックないが1行しかないときは、「{}」不要 (a, b) => a + b // オブジェクトは「()」が必要 (a, b) => ({ a: b, b: a }) // 複数行はいつも通り (a, b) => { const c = a * 100; return { c, d: b / 100, }; }アロー関数
JavaScript アロー関数を説明するよ - Qiita
アロー関数を使って効率的にJavaScriptを記述するimport / export
モジュール毎に機能を管理する時に使います。モジュールで機能を管理することによって、他コードとの依存性を少なくして保守性を高めたり、変数名の競合を防いだり、汎用性の高いモジュールをつくって同じコードを繰り返すのを防いだりできます。
import React from 'react'; import { View, Text, StyleSheet, FlatList, Image, TouchableHighlight, Alert, Linking, } from 'react-native';export default ItemList; export const calories; export function pay(){...} export class Payment {...}import
export
ES6のexportについて詳しく調べた - Qiita非同期処理 async / await
hidouki.js// 非同期のpayメソッド const pay = async (amount) => { const items = await getItems(); const totalAmount = amount + tax; try { await this._payByCreditcard(totalAmount, items); return true; catch(e) { console.warn(e); return false; } };非同期処理には、かなり苦しめられました。Rubyは同期処理できる言語なので、コードを書いたら上から順に実行されます。しかし、Javascriptは非同期であり、たとえばAPIをコールしても、その結果を待たずに、次の行のコードの処理を進めることができてしまうのです。そこで、Promiseオブジェクトを用いて非同期処理を行い、asyncで非同期関数を定義し、awaitで非同期の結果が帰ってくるまで待つことができるのです。
JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる - Qiita
【JavaScript入門】誰でも分かるPromiseの使い方とサンプル例まとめ! | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
async/await 入門(JavaScript) - Qiitaワースト2位:React
React NativeではReactを使ってAndroidアプリとiOSアプリを作ることができます。したがって、React自体の状態管理であったり、JSXなどReact独特の考え方・概念をキャッチアップする必要があります。フロントでReactを使用したことがあれば問題ないのですが、RailsをAPIモードで使用していた経験もなく、SlimでViewを書いていたので、Reactの概念を理解するのも大変でした。
React Native · A framework for building native apps using React
React - ユーザインターフェース構築のための JavaScript ライブラリComponent
ReactはComponent志向で作られます。Railsエンジニアだと、Componentとは?というところのキャッチアップから始める必要があります。Componetをざっくり説明すると、UIを独立させて再利用できるようにして、propsという任意の値を親要素から受け取り画面上に表示させるものです。この文章だけ読んでもわけが分からないと思うので、サンプルコードなどを見ながら理解すると、理解が深まると思います。
// ■ Reactのコンポーネントたち // 【Component】 // Stateを持っていて、再レンダリングの条件を自分でチューニングしたいときに使う。 class FooComponent extends React.Component { render() { return <View />; } } // 【PureComponent】 // Stateを持っていて、再レンダリングの条件をチューニングしないときに使う。 class BarComponent extends React.PureComponent { render() { return <View />; } } // 【Functional Component】 // Stateを持たなくて良い場合に使う。 const FuncComponent = props => { return <View />; };フロントエンドのコンポーネント設計に立ち向かう - Qiita
状態管理(state)
stateとは、Componentの状態を管理するデータです。たとえば、isPayableというフラグがたっていたら支払いのロジックを動かすということをDBでフラグを管理しなくても、フロント側で管理することができるのです。Reactでは、eduxという状態管理のライブラリを使用することが多いのですが、弊社のアプリは軽量なアプリなのでunstatedというライブラリを用いて状態管理をしています。個人的には、Reduxは冗長になってしまうので、unstatedの方が直感的でわかりやすくオススメです。
[React Native] 基本を学ぶ - Stateでコンポーネントの状態を管理する | Developers.IO
コンポーネントの state - React
jamiebuilds/unstated
次の状態管理はReduxをやめてunstatedにする理由 - Qiitaprops
Componentは、propsという任意の引数(正確には引数ではありません)を受け取って、値を当てはめたReact要素を返します。初めてReactを勉強した時は、stateとpropsをごっちゃにしてしまい、それぞれの役割を正確に理解できませんでしたで苦労ポイントです。
[React Native] 基本を学ぶ - Propsでコンポーネントのパラメータを設定する | Developers.IO
コンポーネントと props - ReactJSX
JSXはReactのComponent内でのXML風の構文です。React.jsでは、HTMLでお馴染みの
<h1>
タグなどが利用できるのですが、React Nativeでは、<View>
や<Text>
といった見慣れないタグを扱います。さらに、アプリエンジニアの方は理解しやすいそうなのですが、とかとかアプリ独特のComponentを使用する必要があるので、実際に動かしてみれば、「あーこれか。アプリで使ったことある」と思うのですが、Componentの名前などを覚えるのには一苦労しました。ActivityIndicator · React Native
class Payment extends React.Component { // ここがstateで状態をComponent内で管理している state = { isPayable: true, items: {}, totalAmount: 0, }; _onPressPayment = () => { { isPayable } = this.state; if (isPayable) { const result = await pay(amount); if (result) { this.setState({ isPaylable: false}); } } } render() { return ( // <Text>タグや<TouchableHighLight>がJSX // onPress={this._onPressPayment}がprops <TouchableHighlight onPress={this._onPressPayment}> <Text>支払いをする</Text> </TouchableHighlight> ) }; }ライフサイクルメソッド
ここが個人的には一番ReactNativeエンジニアになって辛かったです。未だに、あれこれってComponentDidMountであってるよね?って確認したくなります。Reactでは複数回renderメソッドが走るので、どのタイミングではどんなデータを用意してなければならないといったことも考慮しなくてはならないので、奥が深いです。さらにReactHooksでの書き換えも「おかんPay」では行っているので、useEffectなどの使い方は悪戦苦闘しています。
lifecycle.js// 色々なライフサイクルメソッドがあります。 componentDidMount() componentDidUpdate(prevProps, prevState, snapshot) componentWillUnmount()【React Native】React Native ライフサイクルメソッドについて|Fresopiya
React component ライフサイクル図 - Qiitaワースト3位:ReactNativeアプリやアプリ開発特有の概念
Web開発では存在しない概念であったり、Web開発の場合と概念が異なることがあり、Railsエンジニアの方にとっては、新しく勉強することが多いです。そこで、自分がReactNativeエンジニアになるにあたって特に苦労したことをまとめました。
React Navigationの画面遷移
画面遷移については、Railsでもある概念です。Routes.rbでルーティングを設定して、redirectやrenderを用いて画面遷移を実現します。しかし、ReactNativeなどアプリの開発では、画面を重ねていく、つまりStackしていくといったイメージの方がしっくりくると思います。RailsでWeb開発だけをしていた自分にとっては、なかなか理解するのに時間がかかるところでした。ライブラリによっても仕様は異なるのですが、弊社ではReact Navigationを使用しています。
React Navigation · Routing and navigation for your React Native apps
React Navigation 3.x チートシートつくってみた - QiitaDeepLinkとかLinking
アプリ同士の連携をするのには、DeepLinkやLinkingというモジュールをReact Native開発では使います。アプリエンジニアにとってはお馴染みの概念なようなのですが、Railsエンジニアの自分にとっては、未知の世界でした。urlを叩いたりするとアプリを開いたりする機能はこうやって実現するのかと素直に感心した領域です。日本語のドキュメントが少ないので、調べるのも英語だったので一苦労でした。今回紹介するのは、React NavigationのDeepLinkになります。
Deep linking · React Navigation
Linking · React Native
ディープリンクをめぐる歴史とReact NativeにFirebase Dynamic Linksを導入する手順 - KitchHike Tech Blogアプリのリリース
iOSとAndroid共にリリースを何度も経験をしているのですが、慣れれば作業なのですが、一番初めのリリースはこんなにも時間がかかるものなのかと驚愕しました。また、弊社プロダクトの「おかんPay」のReactNative版を初リリースしようとしたタイミングでAndroidの審査も始まったらしいというタイミングだったので、てんやわんやでした。さらに、Google Play ConsoleのUIや仕様はかなり頻繁に更新されてしまうので、Qiitaの記事なども最新記事を追わないないと、「そのボタンどこにあるの?」みたいなことは多発しました。初めてのアプリリリースには、余裕をもったスケジュールにすることをオススメします。
iPhoneアプリ申請やAppleの審査に関するメモ - Qiita
Androidアプリにも審査がされるようになった件 - Qiita
React NativeでiOS, Androidのストア公開のTips - QiitaiOSとAndroidのレイアウト差分
iOSとAndroidでは、PickerやCalendarで日付を選択する時など、多くの場合で、レイアウトが異なります。ReactNativeたった1つの言語でiOSとAndroidの両方のアプリを作れることは非常に大きなメリットではあるのですが、両者を全く同じに扱って言い訳ではなく、レイアウトをPlatformによって変更する必要があります。
また、エミュレータと実機の差分もあったり、Androidでは、HuaweiやSonyといった機種依存でレイアウトが変わったり、Androidのバージョンによってもレイアウトが崩れたりします。したがって、Androidすべての機種で実機テストをすることはできないので、レンタルサービスなどを用いてテストしたりしています。
import {Platform, StyleSheet} from 'react-native'; const styles = StyleSheet.create({ width: Platform.OS === 'ios' ? 200 : 100, });【React-Native】iOS, Android両対応する。 - Qiita
配備機種一覧 - 実機検証用スマホ・モバイルデバイスレンタル|テスト受託サービスのケータイラボ最後に
いかがだったでしょうか?Railsエンジニアには馴染みのない概念が多かったと思います。
この記事だけでは、もちろん十分ではないものの、自分がここ半年で苦労した箇所を中心にまとめてあるので、何かの助けになれば幸いです。次は、SocialDogのアプリ開発している@t0m0120さんです。
私もSocialDogは、Twitterを活用するのに利用させてもらっています。どんな記事が読めるのか楽しみです!
- 投稿日:2019-12-05T07:58:56+09:00
|Swiftでバイナリ???を扱う?!>?
Swiftでバイナリファイル扱うことはそんなに頻繁にはないと思うのですが、
案件の中でAndroidチームの人が、巨大なJSONファイルをバイナリに圧縮して処理していて、
iOSアプリでも同じようにできるんじゃないかということで、バイナリを扱うことになったので、その知見を残します。なお僕がやりたかったのは、倍精度浮動小数点数(漢字多すぎですよね)のバイナリファイルを、
Swift上でDouble型の配列に落として使いたい、というのがゴールでした。※バイナリの絵文字として01が出したかったのですが、?しかなかったので、仕方なく?を使っています。
x進数のリテラル
バイナリファイルを扱う前に、Swiftのリテラルを確認しましょう。
x進数let decimalInteger = 17 let binaryInteger = 0b10001 // 17 の 2 進数 let octalInteger = 0o21 // 17 の 8 進数 let hexadecimalInteger = 0x11 // 17 の 16 進数2進数、8進数を直書きすることはあまりないと思います。
0xを先頭につけると16進数扱いになる、が重要です。
特にデバッグで、バイナリファイルじゃなくて、バイナリを扱う処理のロジックを確認したいときに、
サンプルデータとして0x1234みたいにしてよく使いました。バイナリファイルの扱い
バイナリファイルを扱うのがはじめてだったので、何かお作法があるのかなあと思っていましたが、特別なことは何もなく、
バイナリファイルをXcodeから追加して、そのファイルをData型として読むだけでした。let filename = "xxx" //<-バイナリファイル?の名前を入れてね guard let path = Bundle.main.path(forResource: filename, ofType: nil) else { return } guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { fatalError("load failed.") }これで読み込みは完了です?
Data型→String型の変換は意外と簡単
Dataの中身をprintしてみましょう。
何も考えずprintprint(data)結果4016 bytes____________
| あれっ? >?
‾‾‾‾‾‾‾‾‾‾‾‾
Data型はprintしてもByte数が表示されます。
個人的には16進数で吐いてくれると嬉しいのですが、そういう仕様ではありません。
(※NSDataを使うと中身見れます)
単にデバッグなら、Stringに変換するのが楽でした。こちらを参考にしつつ、
Data型→String型print(data.map { String(format: "%02X", $0)})としてやると……
結果["12", "34", "56", "78",……(略)と出力できました〜?
ちなみに。
Stringに変換しない場合、つまり、
Data型→?print(data.map { $0 })とするとどうなるのでしょう?
興味本位でやってみました。結果[12, 52, 86, 120,……(略)____________
| ?! >?
‾‾‾‾‾‾‾‾‾‾‾‾
なんかよくわからないですが、数字が出力されましたね。実はData型のmapはデフォルトがUInt8で処理する仕様なので、
1Byte(8bit)の符号なし整数型として処理されました。
つまり0x12→12
0x34→52
0x56→86
0x78→120と変換がかかったわけです。
さらにちなみに、String(format: "%02X", $0)のformatの指定子ですが、
Stringの公式ドキュメント見ても指定子の詳細なフォーマットが発見できませんでした。
(もし公式知ってる人いたら教えてください?)
C言語のprintf()に指定するフォーマット指定子と一緒らしいので、
Objective-CからSwiftに来ている人には常識なんでしょうか……?
(僕はC言語系ちゃんとやったことないので……)"%02X"で、インプット整数値をアウトプット0詰めアリの2文字の16進数で表示して、という指定になるっぽいです。
Stringも奥が深いですね。浮動小数点数・バイナリを扱う上でのツール
浮動小数点数内部表現シミュレーター
ところで皆さんは浮動小数点数の扱い得意ですか???
頭の中で普通に変換できますか???
僕は無理です。
でも世の中には便利なものがありました。これを駆使して、ロジックが正しいか検証しましょう。
バイナリエディタ
Macの標準アプリで、バイナリを表示できるものはありません(たぶん)。
前職ではWindows環境だったので、Windows上でバイナリ見なきゃいけないときはBzというソフトを使っていた気がします。
(基本的にはメインフレーム入って見てましたが。メインフレーム(というかTSO)は逆にバイナリ見やすかったですね)Mac環境であれば、下記のフリーソフトがいいでしょう。
計算機
Mac標準の電卓アプリ(計算機)を、プログラマモードにすると16進数が扱えるので、活用しましょう???
Data型→数値型の変換
String型に比べると、Data型→数値型の変換はちょっとしんどいです。
なんでこんなめんどくさいんだとムカつきながらやっていましたが、
冷静に考えると、Stringってただのバイトストリームで、バイト長の問題さえ認識があえば処理できるのに対して、
数値型はデータ形式がちょっと複雑なのかなと思ったりしました。
まあでもStringも文字コードが絡むと、文字化けの問題がしんどいですね。。。
あくまで数値のバイナリに限定した話として。Swift5 での Data.withUnsafeBytes
Data型→Double型への変換をゴールとして、話を進めます。
Swift5.0以降で微妙にData型のwithUnsafeBytesの仕様が変わっているので、
テキトーにググってサンプルコード使うと動かないので気をつけてください。
8Byte(64bit)だけ処理するのでいいのであれば、let value = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: Double.self) }でできます。
10進数の1.0が、Doubule型の16進数表記だと0x3FF00000_00000000らしいので、
もしdataの中身が0x3FF00000_00000000であればvalueの値は1.0になります!バイトオーダー
……が、しかし、何度デバッグしても想定していた値とは違う、めちゃくちゃな少数が出てきました。
インプットには正の値しかないはずなのに、なんならマイナスの値が出てきます。iOSはバイトオーダーがリトルエンディアンです。
大事なことなのでもう1度いいますが、iOSはバイトオーダーがリトルエンディアンです。var hoge = UInt32(0x1234ABCD) print(NSData(bytes: &hoge, length: 4))結果<cdab3412>____________
| えー?! >?
‾‾‾‾‾‾‾‾‾‾‾‾つまりどういうことだってばよ
つまりこういうことです。
ビッグエンディアンであれば、0x3FF00000_00000000
↓
3F→F0→00→00→00→00→00→00という順で処理をします。
これは直感的ですね。IBMのメインフレーム(及び互換機)、モトローラのMC68000(及び後継)、サン・マイクロシステムズのSPARC等はビッグエンディアンを採用し、DECのVAX、インテルのx86等はリトルエンディアンを採用している。ARMアーキテクチャ、PowerPCなど、エンディアンを切り替えられるバイエンディアン (bi-endian) のプロセッサも存在する。
言語処理系などの仮想マシンの類では、プラットフォームに応じ使い分ける設計のものもあれば、片方に寄せる設計のものもある。例えば、Java仮想マシンはプラットフォームを問わずビッグエンディアンである。
iPhoneはARMアーキテクチャのCPUなので、バイエンディアンってやつだと思うのですが、デフォルトはリトルエンディアンになっています。
Int型であれば、直前にエンディアン変えられるオプションもあるんですが、Float/Doubleだとちょっと厳しそうですし、
やったとして可読性がはちゃめちゃに悪くなるので、元データの生成をリトルエンディアンでやりなおしました。Data型→[Double]型に
長い道のりでしたが、これで完成です!
let value = data.withUnsafeBytes { Array( UnsafeBufferPointer( start : $0.baseAddress!.assumingMemoryBound( to: Double.self ), count : $0.count / MemoryLayout<Double>.size ) ) }$0にUnsafeRawBufferPointer型のポインタが入っているわけですが、
Arrayに食わせるためにRawじゃないUnsafeBufferPointerにDouble型のアライメントを指定してポインタを再作成しているのが、
ちょっと冗長に感じるので、何かUnsafeRawBufferPointerをちょっと変えてArrayにできないか試行錯誤してみたんですが、結局できませんでした。
Swiftのポインタは雰囲気ではわかるんですが、種類が多くて、イマイチ全容をつかめていない感じがあります。配列に落としたあとは、煮るなり焼くなり。
まとめ
無事Swiftでバイナリファイルを扱うことができました。
Swiftでマジメに低レイヤーの処理するのははじめてで、
「そもそもできるのか?」「結局Objective-Cの方がやりやすかったりするんじゃないの?」と思いながらスタートしましたが、一通りのことはできるみたいです。
Data型でも配列や辞書型みたいに高階関数が使えたのはちょっと感動しましたが、それでもバイナリが出てくると途端に泥臭くなりますね。。。?何かのご参考になれば幸いです。
- 投稿日:2019-12-05T07:54:47+09:00
SwiftUIアプリ開発実践ポイント
こんにちは、たなたつです
![]()
SwiftUIが発表されて半年ほど経ちましたね。あっという間に時間は過ぎていき、iOS 13以降じゃないと使えないし、まだ気にしなくていいでしょなんて言ってられなくなるのもあっという間な気がします。
iOS Advent Calendarの5日目ということで今回は、いくつかSwiftUIでサンプルアプリを作ったり、実際にアプリをリリースしたりした中でたまってきた知見を書こうと思います。
SwiftUIは様々なプラットフォームで動きますがiOSアプリに注目し、開発する前に知っておきたい実践的なポイントなどを共有します。
※ Xcode 11.2.1、iOS 13.2.1 での動作を元に記事を書いています。
SwiftUIの特徴
- 少ないコードでUIを作れる (コードレイアウト)
- 宣言的に記述できる
- Appleのすべてのプラットフォームで動く
- ただし、iOS 13、macOS 10.15、tvOS 13.0、watchOS 6.0以上
このような特徴がよく言われていますが、実際にアプリを作るうえで現状のSwiftUIはどうなのでしょうか。
実際のところSwiftUIってどう?
SwiftUIでiOSアプリをいくつか作ってみてこのように感じました。
- 細かなUIの動きまでアプリの仕様に沿って実装する必要がある場合はかなり難しい
- アプリの仕様をSwiftUIが得意としている仕様に柔軟に変更できる場合は採用しても良い
アプリの仕様通りに細かいUI/UXを実現するのは大変
SwiftUIのAPIはまだUIKitほど柔軟ではないため、UIKitでは実現できるUI/UXを再現できない場合があります。
SwiftUIはUIKitと組み合わせて利用することができるため、SwiftUIではできない部分をUIKitで代わりに実装するというアプローチもできます。
しかし、実際に試してみると組み合わせるために必要なボイラープレートコードが多く、負担になります。また、SwiftUIの特徴的な機能の一つにStateをバインディングしてUIを自動的に更新するというものがありますが、UIKitと組み合わせたときにそれらの機能の活用が難しくなるケースがあります。
そしてそれを回避するためのワークアラウンド的なコードが必要となり、本質的な実装に集中できなくなりがちです。SwiftUIの挙動に合わせてUIを変更可能ならあり
SwiftUIが不得意としている仕様を実装するのは、開発の大きなボトルネックになってしまうため、実験的なアプリや個人アプリのように、アプリの仕様を柔軟に変更できる場合は採用してみてもよいと思いました。 (もちろん対応OSバージョンが狭まることを許容できる場合です)
ユーザーファーストの視点とは全く逆になってしまいますが、開発者視点でUI変更できれば、SwiftUIの強みを活かして爆速でアプリを開発することができるかもしれません。
開発の進め方
ここからは実際にSwiftUIでアプリを作るときにおすすめな開発の進め方を紹介します。
現時点ではSwiftUIの情報が少なく、開発者の知識もUIKitほどはないと思いますので、その状況を想定しています。前述したようにSwiftUIには不得意なUIがあるため、想定しているUIが実現しやすいものなのかどうかを作りながら判断し、難しい場合はアプリの仕様を調整するというサイクルを回していきます。
また、SwiftUIの優れた機能の一つにプレビュー機能があります。
プレビュー機能を使うことで早いサイクルでUIの実現性の確認とレイアウトの調整ができるため、積極的に活用したほうが良いです。画面の漸進的な開発
まずは作りたいレイアウトになるように、Viewの
body
にべた書きしていくとレイアウトしやすいです。struct ListCell: View { var body: some View { HStack { Button(action: { #warning("TODO") }, label: { Image("usericon") .resizable() .scaledToFit() .clipShape(Circle()) .frame(width: 60, height: 60) .padding(8) }) VStack(alignment: .leading) { HStack { Text("たなたつ") Text("@tanakasan2525・10m") .foregroundColor(.gray) } Text("ここは本文が表示されるテキスト領域です。改行することもできます。") .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) // workaround HStack { Button(action: { #warning("TODO") }, label: { Image(systemName: "bubble.left") }) Spacer() Button(action: { #warning("TODO") }, label: { Image(systemName: "arrow.2.squarepath") }) Spacer() Button(action: { #warning("TODO") }, label: { Image(systemName: "heart") }) Spacer() Button(action: { #warning("TODO") }, label: { Image(systemName: "square.and.arrow.up") }) } .foregroundColor(.gray) .padding(8) } } .padding(8) } } struct ListCell_Previews: PreviewProvider { static var previews: some View { ListCell() .previewLayout(.sizeThatFits) } }レイアウトがある程度出来たら、bodyの可読性を上げるためにメソッドに切り出します。
この時にXcodeのリファクタリング機能を使うこともできます。
var body: some View { HStack { userIconView() VStack(alignment: .leading) { userNameView() messageView() bottomButtonView() .padding(8) } } .padding(8) }どこまでメソッド化するか悩ましいですが、bodyを見ればざっくりのレイアウトがわかるくらいまで切り出すのが良いと思います。
また、他の画面でも使いそうなViewのレイアウトはカスタムViewとして切り出していきましょう。
上記の場合、画像のボタンはImageButton
というカスタムクラスを作っても良さそうです。画面のレイアウトのポイント
SwiftUIのエラーは読みにくいので細かく分ける
現状ではSwiftUIのエラーは非常に読みにくく、Xcodeの気持ちを読み取るエスパー力が必要な状態です。
例えばこの実装、どこが悪いでしょうか?
正解は
var body: some View { VStack { Text("エラーわかりにくい") .frame(width: 300, height: 60) TextField("名前", text: self.$viewModel.name) } }TextFieldの第二引数で渡しているtextが期待している型は
Binding<String>
です。エラーの実装は間違えてPublishedのnameに$
をつけてしまっています。ですが、Xcodeが提示しているエラーはなぜか
frame
のところになっています。
上記は短い実装なのでパッと見てわかるかもしれませんがbodyがかなり長い行数になっていた場合、見つけるのは非常に困難です。そのためにもできるだけメソッドやカスタムViewに切り出して
body
部分を短いコードに留めるようにしておきたいです。プレビューしやすいView
Viewを作っていく際に、SwiftUIのプレビュー機能を使うとリアルタイムで表示を確認できるだけでなく、自然と依存関係の少ない (正しい) Viewができていくように思いました。
プレビューするためにはダミーの値を用意してViewに渡す必要があるため、例えば、後述するEnvironmentObject
の良くない使い方をしていると「あれ、プレビューするためにいろいろデータを用意しないといけないぞ、面倒だ」ということに気づき、Viewの粒度やStateの設計などを早いサイクルで改めることができます。なので、積極的にプレビューを利用しながら、プレビューしやすいViewになっているかを常に意識して開発をすると綺麗なViewを保っていけると思いました。
画面遷移の実装
次に画面遷移の実装をします。
画面遷移のよくあるパターンとしては3通りです。
- present (モーダル遷移)
- push (プッシュ遷移)
- 今の画面を新しい画面に置き換える
個人的に感じたSwiftUIでの実装の難易度は簡単順に 3 > 1 >>> 2 です。UIKitでは 1 > 2 > 3 だと思っています。
モーダル遷移
モーダル遷移は
sheet
を使います。@State private var isPresented = false var body: some View { Button("Present") { self.isPresented = true } .sheet(isPresented: $isPresented) { NextView() } }シンプルで柔軟性があり、実装が容易です。
ただし、iOS 13から
pageSheet
スタイルがデフォルトになったため、SwiftUIでもそのスタイルになります。
画面を全部覆うfullScreen
スタイルをSwiftUIで実現するにはUIKitのpresent
を使うか、モーダル遷移アニメーションを自作し、ZStack
などを使って似た表示を再現する必要があります。https://stackoverflow.com/questions/56756318/swiftui-presentationbutton-with-modal-that-is-full-screen
https://stackoverflow.com/questions/56709479/how-to-modally-push-next-screen-to-be-full-in-swiftuiプッシュ遷移
プッシュ遷移は
NavigationLink
とNavigationView
を使います。var body: some View { NavigationView { NavigationLink("Push", destination: NextView()) .navigationBarTitle("Title") } }リンクボタンをタップしてプッシュ遷移する場合は、このようにシンプルになりますが、例えば、通信成功後にプッシュ遷移する場合はこのようになります。
@State private var isPushed = false var body: some View { NavigationView { VStack { Button("Fetch data") { // 通信の代わりに遅延させる DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // データの取得後Push self.isPushed = true } } // 見えないリンクを置いて遷移先を設定する NavigationLink(destination: NextView(), isActive: $isPushed, label: EmptyView.init) } .navigationBarTitle("Title") } }ちょっと違和感のある実装になってしまいますね。調べた限り、今のAPIではこのようになります。
そして、非同期で取得したデータを次の画面に渡す方法はどのようになるでしょうか。
何通りもやり方はありますが、素直に実装する場合はこのようになると思います。@State private var isPushed = false @State private var fetchedData: String? var body: some View { NavigationView { VStack { Button("Fetch data") { // 通信の代わりに遅延させる DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // データの取得後Push self.isPushed = true // 取得したデータの設定(new dataというデータを取得できたとする) self.fetchedData = "new data" } } if fetchedData != nil { // ViewBuilder内ではif letは使えない NavigationLink(destination: NextView(data: fetchedData!), isActive: $isPushed, label: EmptyView.init) } // if letの代わりにmapを使うこともできる(こっちの方が綺麗) // fetchedData.map { // NavigationLink(destination: NextView(data: $0), isActive: $isPushed, label: EmptyView.init) // } } .navigationBarTitle("Title") } }ちょっとずつらみが出てきましたね。画面遷移とデータ渡しがセットになっているケースを単純に実装するとプロパティの数がどんどん増えてしまいます。
そのため、ObservableObject
やカスタムView、structでデータをきれいにまとめるなどの工夫によって、Viewを清潔に保つように頑張る必要があります。この辺りは次のState設計の部分でいくつかパターンを紹介します。
プッシュ遷移周りはUIKitよりも明らかに面倒です。ナビゲーションバーやエッジスワイプ周りでも厄介な部分があるので、後で軽く紹介します。
今の画面を新しい画面に置き換える
今の画面をまるっと新しい画面に置き換える実装はかなり簡単です。
@State private var isBlueView = false var body: some View { VStack { if isBlueView { BlueView() } else { RedView() } Button(isBlueView ? "Red" : "Blue") { self.isBlueView.toggle() } } }bodyの中でif文を使うことができるので、そこで表示するViewを出し分けるだけで簡単に画面切替が可能です。
画面遷移実装のポイント
基本的にはUX優先で遷移方法を選択して良いと思います。ですが現在のSwiftUIのバグなどによっては問題を回避するワークアラウンドを考えるよりも遷移方法を見直すほうが良い場合も多いため、柔軟に仕様を変えられるようにしておきたいところです。
執筆時現在に起きているいくつかの問題/複雑なポイントを紹介します。
NavigationBarItemに置いたNavigationLinkでPushした後、Popするとクラッシュする
ナビゲーションバーにボタンを置いてPush遷移する動作はよくあるものですが、iOS 13.2 では
NavigationBarItem
に置いたNavigationLink
でPushした後、Popするとクラッシュしてしまいます。https://forums.developer.apple.com/thread/124757
回避方法としては
NavigationLink
ではなく、普通のButton
を置くようにし、前述したようなEmptyView
を持つNavigationLink
を用いて画面遷移するようにするとうまくいきます。navigationBarHiddenは親の設定が優先される
struct ContentView: View { var body: some View { NavigationView { NavigationLink("Push", destination: NextView()) .navigationBarHidden(true) .navigationBarTitle("") } } } private struct NextView: View { var body: some View { Color.blue .navigationBarHidden(false) .navigationBarTitle("Next View") } }このようなコードなら、プッシュ後にナビゲーションバーが表示されるようになりそうですが、実際はこのようになります。
動作を観察すると、ViewGraphの親の設定が優先されるようでした。
この現象を回避するためにはこのように親側の状態を更新する必要があります。
struct ContentView: View { @State private var isPushed = false var body: some View { NavigationView { NavigationLink(destination: NextView(), isActive: $isPushed, label: { Text("Push") }) .navigationBarHidden(!isPushed) .navigationBarTitle("") } } } private struct NextView: View { var body: some View { Color.blue .navigationBarTitle("Next View") } }sheetをメソッドチェインor入れ子にすると最後(親)のsheetしか動かなくなる
複数のモーダルを表示したいときに以下のように書きたくなりますが、Modal 1が動かなくなります。
struct SheetChain: View { @State private var isModal1Presented = false @State private var isModal2Presented = false var body: some View { VStack { Button("Modal 1") { self.isModal1Presented = true } Button("Modal 2") { self.isModal2Presented = true } } .sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) }) .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) }) } } private struct NextView: View { let color: Color var body: some View { color } }
sheet
がチェインしたり入れ子になっていると、最後(親)のsheet
しか動かなくなるようです。これを回避するためには
sheet
がチェインしないように各ボタンにsheet
を付けるようにします。struct SheetChain: View { @State private var isModal1Presented = false @State private var isModal2Presented = false var body: some View { VStack { Button("Modal 1") { self.isModal1Presented = true } .sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) }) Button("Modal 2") { self.isModal2Presented = true } .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) }) } } }シンプルな画面であればあまり問題になりませんが、複雑な画面でViewを細かくコンポーネント化し、入れ子にしていくと発生しやすいので
sheet
はできるだけ子のViewにつけるようにしたほうが良いです。
ちなみに入れ子で動かなくなるというのはこのような例です。var body: some View { VStack { Button("Modal 1") { self.isModal1Presented = true } Button("Modal 2") { self.isModal2Presented = true } .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) }) }.sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) }) }この場合は、Modal 2が動かなくなります。最後(親)のsheetしか動かなくなるためです。
State設計
SwiftUIの便利な機能の一つであるバインディングに必要なStateはアプリを作り進めていくとだんだんと増えていき、Viewの可読性が落ちていきがちです。
そこで、Stateをできるだけきれいに保つためにいくつかの便利なテクニックを紹介します。
関連性のあるStateはstructにまとめる
いろいろなサンプルコードで
@State
や@Published
をプリミティブ型に対して利用していることが多いですが、structでも利用可能です。前述したAPIからデータを取得した後にPush遷移する時のStateはこのように書くこともできます。
struct NavigationStateWithData<T> { var isActive: Bool = false { didSet { if !isActive, data != nil { data = nil } } } var data: T? { didSet { // 無限ループしないように代入前のチェックが必要 if (data == nil) == isActive { isActive.toggle() } } } } struct ContentView: View { @State private var fetchedData = NavigationStateWithData<String>() var body: some View { NavigationView { VStack { Button("Fetch data") { // 通信の代わりに遅延させる DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.fetchedData.data = "new data" } } fetchedData.data.map { NavigationLink(destination: NextView(data: $0), isActive: $fetchedData.isActive, label: EmptyView.init) } } .navigationBarTitle("Title") } } }まとまるとStateの関連性がわかりやすくなってよいですね。
上記の場合はデータがあるときに自動でPush遷移をするカスタムNavigationLinkを作るのもありかもしれません。ObeservableObject
@State
とstructの組み合わせでは表現しにくいViewの全体の状態を管理する時はObservableObject
を使います。
ObservableObject
はクラスにしか適合できないプロトコルのため、structでは値がコピーされてしまうようなViewを跨ぐ状態管理や、値更新時にViewを更新する必要がないプロパティなどを保持したりするのに便利です。また、
ObservableObject
で使用する@Published
は$
でアクセスすることで値を監視できるPublisher
として扱えるので、値の変化に伴って処理を挟むことができます。struct SettingView: View { @ObservedObject private var viewModel = SettingViewModel() var body: some View { Picker("テーマ", selection: $viewModel.theme) { Text("ライトモード").tag(UIUserInterfaceStyle.light) Text("端末の設定に従う").tag(UIUserInterfaceStyle.unspecified) Text("ダークモード").tag(UIUserInterfaceStyle.dark) }.pickerStyle(SegmentedPickerStyle()) } } class SettingViewModel: ObservableObject { @Published var theme = UIUserInterfaceStyle.unspecified private var cancellables: Set<AnyCancellable> = [] init() { $theme.sink { [weak self] theme in // 外観モードを切り替える let keyWindow = UIApplication.shared.windows.first { $0.isKeyWindow } keyWindow?.overrideUserInterfaceStyle = theme }.store(in: &cancellables) } }ロジックを
ObservableObject
に詰め込むと状態の更新タイミングが複雑になりがちです。できるだけUIに関係のある処理だけをここに記述するようにして、複雑なロジックは別の型に記述したほうが良いと思いました。EnvironmentObject
EnvironmentObject
は子View全てにオブジェクトを伝搬させることができる機能で、これを使うとViewを細かくコンポーネント化していったときに毎回initでオブジェクトを渡す必要がなくなるためとても便利です。ただ、なんでも
EnvironmentObject
で渡してしまうと、データがどこで更新されたのか分かりにくくなったり、Viewの使いまわしにくくなったりします。実際に使ってみて思ったGood/Badパターンはこちらです。
EnvrionmentObjectのGoodパターン
- アプリ全体で使う表示に関係する状態を管理する
- ログイン状態など
- 他の画面で使いまわさない子Viewにオブジェクトを渡す
- Fluxで実装した場合のStoreを子Viewに渡すときなど
EnvrionmentObjectのBadパターン
- 表示に全く関係しない状態を管理する
- それはシングルトンなオブジェクトで管理するほうが適しているかもしれません
- 子Viewに必要のないデータを含むオブジェクトを
EnvironmentObject
で渡す
- そのデータの監視方法は
@State
や@ObservedObject
に置き換えられるかもしれません- 他の画面でも使い回されるViewが特定のViewに依存した
EnvironmentObject
を参照している
- そのデータは
init
で渡すようにしたほうが良いかもしれませんState実装時のTips
Single Source of Truth
Appleは「Single Source of Truth」を推奨しています。これはデータソースは1つにしましょうという意味です。
SwiftUIの実装的には同じ意味を持つデータを別々の
@State
や@Published
で保持しないようにするということになります。値を保持せず参照だけしたい時は@Binding
を使いましょう。Bindingを使ったチュートリアル
https://developer.apple.com/tutorials/swiftui/handling-user-inputまた、あるStateに変更があった時に別のStateを変更したいというケースでは、Combineフレームワークを使って値を監視すると、同一ソースを複数のStateで保持する (Single Source of Truthに反する) 必要があるときにも多少安全です。
// ユーザーの入力によってリストの表示をフィルターする処理 class SearchViewModel: ObservableObject { @Published var keyword = "" @Published private(set) var items: [Item] = [] @Published private(set) var filteredItems: [Item] = [] // ↑computed propertyにすることも可能ですが、結果をキャッシュしたいという意図です // ... private var cancellables: Set<AnyCancellable> = [] init() { // 入力キーワードがnameに含まれているものをfilteredItemsにセットする $keyword.combineLatest($items).map { keyword, items in items.filter { item in item.name.localizedCaseInsensitiveContains(keyword) } } .assign(to: \.filteredItems, on: self) .store(in: &cancellables) } }
@State
プロパティはprivate
に
@State
のプロパティをViewのbody
外から操作すると実行時エラーになります。
想定外の用途を防ぐために、@State
のプロパティにはprivate
を付けるようにしたほうが良いです。こちらはSwiftUIのドキュメントにも明記されています。
you should declare your state properties as private, to prevent clients of your view from accessing it.
https://developer.apple.com/documentation/swiftui/stateまた、
@Published
のプロパティもprivate(set)
にできるケースは結構多いので、できるだけViewを更新可能な人物を減らすように意識していくと良いと思います。よく使うサービス/ツールとの相性
fastlane
今のところ何も問題なく利用できています。App Store Connectへのアップロードも全く問題ありませんでした。
Firebase Crashlytics
SwiftUIのViewはView BuilderとOpaque Result TypeによってView構造が型になっているため、クラッシュログのスタックトレースがこのようになります。
※自作の型名などはマスク処理しています。
一見複雑ですが、よく見るとViewのどの部分から起きている問題なのかが以外と分かるため、特に大きな問題は感じていません。
Admob
Admobを利用する場合は、ネイティブ広告で自前のSwiftUI Viewを組み立てるか、またはAdmobが提供しているUIKitのインターフェースをラップして利用することになります。
ラップして利用する場合はこのような実装になります。
バナー
struct AdBanner: UIViewControllerRepresentable { let adUnitId: String private var adSize: GADAdSize { UIDevice.current.userInterfaceIdiom == .pad ? kGADAdSizeFullBanner : kGADAdSizeBanner } func expectedFrame() -> some View { let size = adSize.size return frame(width: size.width, height: size.height, alignment: .center) } func makeUIViewController(context: Context) -> UIViewController { let view = GADBannerView(adSize: adSize) let viewController = UIViewController() view.adUnitID = adUnitId view.rootViewController = viewController viewController.view.addSubview(view) viewController.view.frame.size = adSize.size view.load(GADRequest()) return viewController } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } // body内 --- AdBanner(adUnitId: "***").expectedFrame()インタースティシャル
final class Interstitial: NSObject { private let adUnitId: String private var interstitial: GADInterstitial! private var completion: (() -> Void)? required init(adUnitId: String) { self.adUnitId = adUnitId super.init() load() } private func load() { interstitial = GADInterstitial(adUnitID: adUnitId) interstitial.load(GADRequest()) interstitial.delegate = self } func show(completion: @escaping () -> Void) { guard canShow(), interstitial.isReady, // ViewControllerの取得処理を簡略化していますが、場合により適切なWindowを選択して取得するように変える必要があります let root = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController else { completion() return } self.completion = completion interstitial.present(fromRootViewController: root) // 何度も表示されないように調整する場合はこの辺りに処理を書く } private func canShow() -> Bool { // 条件を満たしたら表示する return true } } extension Interstitial: GADInterstitialDelegate { func interstitialDidDismissScreen(_ ad: GADInterstitial) { completion?() load() // 再表示に備えて再読み込み } } // View内 --- private let interstitial = Interstital(adUnitId: "***") var body: some View { YourCustomView() .onAppear { self.interstital.show { // 広告を閉じた後の処理 } } }まとめ
現段階でSwiftUIを使う時の進め方や注意点、Tipsなどを紹介しました。
まだまだAPIが足りず、複雑な画面仕様を実現するには難しいケースもありますが、ある程度SwiftUIにアプリの仕様を寄せることができれば使っても良さそうです。
UIKitでレイアウトを作るよりも圧倒的に早く見た目が作れ、作った後のレイアウト変更も簡単なので、SwiftUIのバグさえ回避できればかなり楽にアプリが作れました。SwiftUIの破壊的変更や不具合と戦いながらその進化を見ていくのはエンジニアとしては面白い経験かと思うので、SwiftUIアプリ作りに挑戦してみてはどうでしょうか。
その他参考
- 投稿日:2019-12-05T02:01:27+09:00
[iOS] Bitriseのキャッシュをできるだけ生かしてCI / CDの高速化を図る
はじめに
この投稿はCyberAgent Developers Advent Calendar 2019 4日目の記事です!
本日はTapple iOSチームに今年加入した、19新卒の @nade が担当します。
今回はTappleのiOSアプリのCIのワークフローをキャッシュをなるべく生かす設定に変えることで、
Bitriseのワークフローを 40%〜50% ほどの高速化に成功した話を書きたいと思います。高速化前後のワークフロー
現在チームではBitrise上でテストを自動化しています。
ざっくり下記のようなワークフローです。
- リポジトリのクローン
- Homebrewパッケージのインストール(Xcodegen, Linterなど)
- Gemパッケージのインストール(xcov, CocoaPods, danger, fastlaneなど)
- Cocoapodsのインストール
- テストの実行
下記は高速化前のワークフローの実際のサマリー(最後に表示される実行時間)です。
高速化前+------------------------------------------------------------------------------+ | bitrise summary | +---+---------------------------------------------------------------+----------+ | | title | time (s) | +---+---------------------------------------------------------------+----------+ | ✓ | activate-ssh-key | 11.26 sec| +---+---------------------------------------------------------------+----------+ | ✓ | git-clone | 1.5 min | +---+---------------------------------------------------------------+----------+ | ✓ | Set Env Path | 5.94 sec | +---+---------------------------------------------------------------+----------+ | ✓ | cache-pull | 4.0 min | +---+---------------------------------------------------------------+----------+ | ✓ | Bundle Install | 50.90 sec| +---+---------------------------------------------------------------+----------+ | ✓ | Brew Install | 1.1 min | +---+---------------------------------------------------------------+----------+ | ✓ | XcodeGen | 15.0 sec| +---+---------------------------------------------------------------+----------+ | ✓ | CocoaPods Install | 4.1 min | +---+---------------------------------------------------------------+----------+ | ✓ | cache-push | 25.18 sec| +---+---------------------------------------------------------------+----------+ | ✓ | github-status | 5.69 sec | +---+---------------------------------------------------------------+----------+ | ✓ | Fastlane Test | 19.0 min | +---+---------------------------------------------------------------+----------+ | ✓ | cache-push | 15.52 sec| +---+---------------------------------------------------------------+----------+ | ✓ | github-status | 5.19 sec | +---+---------------------------------------------------------------+----------+ | Total runtime: 31.9 min | +------------------------------------------------------------------------------+この中で特に、 Homebrew , Gem , Cocoapods (アプリビルド)のそれぞれの設定を見直し、
よりキャッシュを生かすことで、下記のようにワークフローの合計時間を40%〜50%ほど高速化することができました。高速化後+------------------------------------------------------------------------------+ | bitrise summary | +---+---------------------------------------------------------------+----------+ | | title | time (s) | +---+---------------------------------------------------------------+----------+ | ✓ | activate-ssh-key | 10.15 sec| +---+---------------------------------------------------------------+----------+ | ✓ | Git Config | 3.17 sec | +---+---------------------------------------------------------------+----------+ | ✓ | git-clone | 29.22 sec| +---+---------------------------------------------------------------+----------+ | ✓ | Set Env Path | 4.44 sec | +---+---------------------------------------------------------------+----------+ | ✓ | cache-pull | 31.66 sec| +---+---------------------------------------------------------------+----------+ | ✓ | Brew install | 8.13 sec | +---+---------------------------------------------------------------+----------+ | ✓ | XcodeGen | 11.53 sec| +---+---------------------------------------------------------------+----------+ | ✓ | rbenv Install | 4.48 sec | +---+---------------------------------------------------------------+----------+ | ✓ | Bundle Install | 5.07 sec | +---+---------------------------------------------------------------+----------+ | ✓ | CocoaPods Install | 38.11 sec| +---+---------------------------------------------------------------+----------+ | ✓ | cache-push | 9.71 sec | +---+---------------------------------------------------------------+----------+ | ✓ | github-status | 6.70 sec | +---+---------------------------------------------------------------+----------+ | ✓ | Fastlane Test | 13.6 min | +---+---------------------------------------------------------------+----------+ | ✓ | github-status | 7.55 sec | +---+---------------------------------------------------------------+----------+ | Total runtime: 16.4 min | +------------------------------------------------------------------------------+※bitrise summaryのTotal runtimeには時間帯、環境によって増減します。
記事内のものは同時刻帯のものを選んで比較していますが、他の要因も影響もあるかもしれません
補足
上記の高速化前のワークフローでは、
1../Pod
(Podfile.lockに変更があった場合)
2./Users/vagrant/Library/Caches/Homebrew
3.~/Library/Developer/Xcode/DerivedData
がキャッシュされていました。
高速化後のワークフローでは、2, 3 を効果がみられなかったため、削除しています。Bitriseでのキャッシュ機能について
Bitriseの公式のドキュメントにはキャッシュの機能についての記述があります。
いくつか抜粋すると
- キャッシュは全てのキャッシュされたディレクトリや依存性をtarし、Amazon S3内に安全に保存される
a/path/to/cache
を指定した場合、/path/to/cache/.ignore-me
もキャッシュされる- キャッシュはブランチ単位で管理される
- PR内ではcache-pushは行われない
- そのブランチ上で新しいビルドが行われなかった場合、7日後に有効期限が切れて削除される
利用方法
利用方法は、2つのステップを追加することで利用できます。
Bitrise.io Cache:Pull step to download the previous cache (if any).
Bitrise.io Cache:Push step to check the state of the cache and upload it if required.bitriseのワークフローに下記のように記述することで、選択したディレクトリ以下をキャッシュすることができます。
bitrise.ymlworkflows: workflow-a: steps: # 他のワークフロー - cache-pull: {} #pullはこれだけ # 他のワークフロー - cache-push: inputs: - cache_paths: |- $BITRISE_CACHE_DIR MyProject/Cache #キャッシュしたいパス ./Pods -> ./Podfile.lock #Podfile.lockに変更があれば./Podsをキャッシュ
Podfike.lock
の例のように変更を監視するファイルを指定することができますが、これはdiffを見ていないわけではなく、Cleaning paths Done in 1.32941661s Checking previous cache status Previous cache info found at: /tmp/cache-info.json Done in 171.741223ms Checking for file changes 0 files needs to be removed 0 files has changed 0 files added No files found in 37.785423ms Total time: 1.539638885s | | +---+---------------------------------------------------------------+----------+ | ✓ | cache-push | 8.99 sec | +---+---------------------------------------------------------------+----------+ログを確認すると、削除、変更、追加のそれぞれを確認していることがわかります。
なので、変更を監視するファイルを指定することで、このフローを簡略化することができます。効果があったキャッシュ設定
以降で効果的だったキャッシュ方法を紹介します。
Homebrewのキャッシュ
公式ドキュメント -Caching Homebrew installersでは下記のような方法の記載があります。
The Brew install Step supports caching: if the option is enabled, any downloaded brew installers will be cached from the location of brew --cache. The cache path is ~/Library/Caches/Homebrew/.
To enable caching of brew installers:
- Go to the Workflow in which you want to cache brew installs and select the Brew install Step.
- Set the Cache option to yes.
- As always, click Save.
公式では、Homebrewの
--cache
オプションを指定していますが、これでは毎回インストールが走ることは避けられません。これはバイナリを直接リンクする方法で、インストール済みのfomulaの再インストールを回避することができます。
(~/Library/Caches/Homebrew
はキャッシュする必要ないため、公式のStepは使わない)- script: inputs: - content: |- # キャッシュするディレクトリを環境変数に追加 envman add --key BREW_XCODEGEN --value "$(brew --cellar)/xcodegen" #パッケージ名を指定 envman add --key BREW_OPT_XCODEGEN --value "/usr/local/opt/xcodegen" #パッケージ名を指定 title: Set Env Path - script: inputs: - content: |- #インストールは以下のコマンドを実行 brew install xcodegen brew link xcodegen title: Brew install - cache-push: inputs: - cache_paths: |- $BITRISE_CACHE_DIR #以下の二つをcache_pathに追加 $BREW_XCODEGEN $BREW_OPT_XCODEGENキャッシュが成功している時のログ+ brew install xcodegen Warning: xcodegen 2.10.1 is already installed, it's just not linked You can use `brew link xcodegen` to link this version. + brew link xcodegen Linking /usr/local/Cellar/xcodegen/2.10.1... 2 symlinks createdこれにより
brew install
ステップを80%ほど短縮することができました。高速化前+---+---------------------------------------------------------------+----------+ | ✓ | Brew Install | 1.1 min | +---+---------------------------------------------------------------+----------+高速化後+---+---------------------------------------------------------------+----------+ | ✓ | Brew install | 8.13 sec | +---+---------------------------------------------------------------+----------+また、この方法だと1つ以上インストールが走った場合に、毎回
brew cleanup
が走り時間を使ってしまいます。
これは、HomebrewのGithubリポジトリ内のcleanup.rbを確認すると、
~/Library/Caches/Homebrew/.cleaned
を使って条件を判別しているため、こちらをキャッシュ対象に追加することで回避できます。Ruby Gems
rbenv
公式ドキュメント -Caching Ruby Gemsでは下記のような方法の記載があります。
手元の環境だとキャッシュパスが
~/.rbenv/versions/2.6.3
を差しています。これはrbenvのキャッシュ指定ですね。
Bitriseから提供されてるスタックだと ruby 2.6.3 はインストールされていないため、この設定は有効でした。また、リポジトリ内で
.ruby-version
を管理している場合は、push時にはここをチェックすれば良さそうです。gem
さらに、gemのインストール先ディレクトリ(tappleでは
./vendor/bundler
)をキャッシュすることで、
bundle install
時のインストールも回避することができます。
こちらはGemfile.lockに変更が反映されるので、push時のチェックに指定します。- script: title: Bundle Install inputs: - content: |- bundle install --path vendor/bundle - cache-push: inputs: - cache_paths: |- #以下の二つをcache_pathに追加 $GEM_CACHE_PATH -> ./.ruby-version #公式ドキュメント↑の環境変数 ./vendor -> ./Gemfile.lock #gemのインストール先ディレクトリを指定キャッシュが成功している時のログ+ bundle install --path vendor/bundle Using rake 13.0.1 Using CFPropertyList 3.0.1 ... Using xcprofiler 0.6.3 Bundle complete! 9 Gemfile dependencies, 109 gems now installed. Bundled gems are installed into `./vendor/bundle`これにより
bundle install
を90%ほど短縮することができました。高速化前+---+---------------------------------------------------------------+----------+ | ✓ | Bundle Install | 50.90 sec| +---+---------------------------------------------------------------+----------+高速化後+---+---------------------------------------------------------------+----------+ | ✓ | Bundle Install | 5.07 sec | +---+---------------------------------------------------------------+----------+CocoaPods
公式ドキュメント -Caching Ruby Gemsでは下記のような方法の記載があります。
Before you start, make sure you have the latest version of the Cocoapods Install Step in your Workflow.
- Open your app’s Workflow Editor.
- Insert the Cache:Pull Step after the Git Clone but before the Cocoapods Install Steps.
IMPORTANT: Make sure that your Step is version 1.0.0 or newer. With the older versions, you have to manually specify paths for caching.- Insert the Cache:Push step to the very end of your workflow.
Bitrise公式が用意した
Cocoapods Install
を指定するだけでよしなにやってくれるそうですが、
個人的には公式のステップでは、
ログを確認すると、実行時に
- Check selected Ruby is installed
-$ gem install bundler --force
-bundle install
-pod install
と複数の作業をしてくれてるのですが、 前ステップの工程と被っている部分があります。
なので、bundle exec pod install
単体を利用しています。正しいbundle, rbenvがステップ以前にインストールされていれば、
Podsファイルをキャッシュパスに指定し、Podfile.lockをチェックするだけで短縮できます。- script: inputs: - content: |- bundle exec pod install title: CocoaPods Install - cache-push: inputs: - cache_paths: |- ./Pods -> ./Podfile.lock ##キャッシュパスに追加高速化前のフローでは毎回
--repo-update
オプションを指定していましたが、シチュエーションを絞ったり、
常に有効なCocoaPodsのキャッシュがある状態にしておくことで、85%ほど短縮することができました。高速化前+---+---------------------------------------------------------------+----------+ | ✓ | CocoaPods Install | 4.1 min | +---+---------------------------------------------------------------+----------+高速化後+---+---------------------------------------------------------------+----------+ | ✓ | CocoaPods Install | 38.11 sec| +---+---------------------------------------------------------------+----------+キャッシュが成功している時のログ+ bundle exec pod install Using FBSDKCoreKit Using FirebaseDynamicLinks Using Crashlytics Using FirebaseCoreDiagnosticsInterop Using FirebaseCore Using GoogleDataTransport Using GoogleDataTransportCCTSupport ・・・さらなる高速化するために
高速化前のサマリーを見たときに一番目につくのが
Fastlane Test
の部分です。高速化前+---+---------------------------------------------------------------+----------+ | ✓ | Fastlane Test | 19.0 min | +---+---------------------------------------------------------------+----------+このステップには
- アプリのビルド
- テスト
- Danger
- Slackへの通知
が含まれています。
この部分をキャッシュを使って高速化する手段はないのでしょうか?
cocoapods-binary
残念ながら、現在BitriseはXcodeのビルドキャッシュには対応していませんが、
cocoapods-binary
を使った CocoaPodsの_Prebuild であればキャッシュすることができます。参考: https://github.com/leavez/cocoapods-binary
導入方法は省略しますが、
./Pods/_Prebuild
(Pods下なので追加のキャッシュパスの指定なし)
をキャッシュすることで、Podsのビルドを毎回する必要がなくなるため、
大幅にCI上でのビルド時間を短縮することができました。
30%の改善ですが、5.4mと短縮できた時間はここが一番大きかったです。高速化前+---+---------------------------------------------------------------+----------+ | ✓ | Fastlane Test | 19.0 min | +---+---------------------------------------------------------------+----------+高速化後+---+---------------------------------------------------------------+----------+ | ✓ | Fastlane Test | 13.6 min | +---+---------------------------------------------------------------+----------+最後に
今回はBitriseのキャッシュをできるだけ生かして高速化できるポイントを紹介しました。
しかし、忘れてはならないのは、Cどこかで不整合が起きるリスクを必ず孕んでいるということです。Bitriseのキャッシュのログをみると、Pullする前にStackが同一であることを確認してくれてます。
+------------------------------------------------------------------------------+ | (4) cache-pull | +------------------------------------------------------------------------------+ | id: cache-pull | | version: 2.1.2 | | collection: https://github.com/bitrise-io/bitrise-steplib.git | | toolkit: go | | time: 2019-12-02T15:03:01Z | +------------------------------------------------------------------------------+ | | Config: - CacheAPIURL: [REDACTED] - DebugMode: false - StackID: osx-xcode-11.2.x Downloading remote cache archive Checking archive and current stacks current stack id: osx-xcode-11.2.x archive stack id: osx-xcode-11.2.x Extracting cache archive Doneしかし、 ライブラリのアップデート時にキャッシュを破棄する 等のTTLを設ける。
いつでもキャッシュを更新、無効にする方法を用意しておく等の対策は必要です。
今回の実装で、キャッシュに頼りつつも、依存しすぎない関係を築くことが大事だなあと感じました。参考
- 投稿日:2019-12-05T01:42:37+09:00
日本語で書かれたLocalizable.stringをGoogle Translation APIで翻訳してstructファイルを自動作成するツールをgolangで作る
はじめに
以前Qiitaの記事でも書いた「Go言語でiOS開発ツールを作成する:Localizable.stringsファイルからenumを生成する」というツールを拡張して、
「日本語で書かれたLocalizable.stringをGoogle Translation APIで翻訳してstructファイルを自動作成するツールをgolangで作る」
ことに挑戦してみました。
挑戦結果のGitHubリポジトリーはこちら https://github.com/BlueEventHorizon/EnumGenerator結果
例えば下記のようなLocalizable.stringがあったとすると、
"あなたの心が正しいと思うことをしなさい。どっちにしたって批判されるのだから。" = "あなたの心が正しいと思うことをしなさい。どっちにしたって批判されるのだから。"; "前進をしない人は、後退をしているのだ。" = "前進をしない人は、後退をしているのだ。"; "どんなに悔いても過去は変わらない。どれほど心配したところで未来もどうなるものでもない。いま、現在に最善を尽くすことである。" = "どんなに悔いても過去は変わらない。どれほど心配したところで未来もどうなるものでもない。いま、現在に最善を尽くすことである。"; "最も重要な決定とは、何をするかではなく、何をしないかを決めることだ。" = "最も重要な決定とは、何をするかではなく、何をしないかを決めることだ。"; "人生は楽ではない。そこが面白い。" = "人生は楽ではない。そこが面白い。"; "自分で自分をあきらめなければ、人生に「負け」はない。" = "ダイアログを自分で自分をあきらめなければ、人生に「負け」はない。";生成結果は以下のようになりました。
Swiftimport Foundation struct LocalizableStrings { static let doWhatYouThinkIsRightBecauseYouAre = "あなたの心が正しいと思うことをしなさい。どっちにしたって批判されるのだから。" static let thoseWhoDoNotMoveForwardAreMovingBackwards = "前進をしない人は、後退をしているのだ。" static let thePastDoesntChangeNoMatterHowMuch = "どんなに悔いても過去は変わらない。どれほど心配したところで未来もどうなるものでもない。いま、現在に最善を尽くすことである。" static let theMostImportantDecisionIsNotWhatYou = "最も重要な決定とは、何をするかではなく、何をしないかを決めることだ。" static let lifeIsNotEasyThatIsInteresting = "人生は楽ではない。そこが面白い。" static let ifYouDontGiveUpYourselfThereIsNoLosing = "自分で自分をあきらめなければ、人生に「負け」はない。" }そもそも
Localizable.string
の記述が変❗️というのは置いておいてください・・・翻訳
go言語を使ってGoogle Cloud Platformを利用するのはSwiftよりも有利です。それは、Googleから
- 十分なサンプルソースコードが提供されている。
- go言語用のライブリが提供されている。
からです。
サンプルソースコードは下記から入手することができます。https://github.com/GoogleCloudPlatform/golang-samples
またこのサンプルソースコード内で利用されているライブラリは、
下記のように組み込むことができます。Shell$ go get -u cloud.google.com/go/translate $ go get -u golang.org/x/text/language $ go get -u google.golang.org/api/optionこのライブラリを利用すると、go言語からは下記のようにシンプルなコードで済んでしまいます。
Gofunc TranslateText(targetLanguage, text string) (string, error) { ctx := context.Background() lang, err := language.Parse(targetLanguage) if err != nil { return "", err } client, err := translate.NewClient(ctx) if err != nil { return "", err } defer client.Close() resp, err := client.Translate(ctx, []string{text}, lang, nil) if err != nil { return "", err } result := resp[0].Text return result, nil }Google Translation APIを利用するには
Quickstart (Basic)
https://cloud.google.com/translate/docs/basic/setup-basicから簡単に始めることができます。
- GCP Console プロジェクトをセットアップする
- 環境変数
GOOGLE_APPLICATION_CREDENTIALS
を設定するGCP Console プロジェクトをセットアップは上記のリンクから行えます。
またGOOGLE_APPLICATION_CREDENTIALSに設定すべき「サービスアカウントキーをが含むJSON」も上記で入手できますので、例えばmacの場合だと自分のホームディレクトリに置き、下記のようにタイプすると一時的に使用可能になります。Shell$ export GOOGLE_APPLICATION_CREDENTIALS=~/xxxxxx.json恒久的に設定するには、bash環境であれば、
.bashrc
や、.bash_profile
に書き込みましょう。VSCodeでのデバッグ
VSCodeでは、上記のシェル環境のGOOGLE_APPLICATION_CREDENTIALSが有効では無いために、別途設定が必要です。
macであれば、.vscodeの下に、launch.jsonというファイルができれいるはずなので、launch.json{ // IntelliSense を使用して利用可能な属性を学べます。 // 既存の属性の説明をホバーして表示します。 // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Launch", "type": "go", "request": "launch", "mode": "auto", "program": "${fileDirname}", "env": { "GOOGLE_APPLICATION_CREDENTIALS": "クレデンシャルへのフルパス/xxxxx.json" }, "args": [""] } ] }"env"にクレデンシャルへのフルパスで書きます。なぜか、 ~/xxxxx.json というような記法ではダメなようです。
go言語のサンプルソースコードについて
多すぎて書ききれないので割愛します。
こちらを見るだけでその多さがわかると思います。https://github.com/GoogleCloudPlatform/golang-samples
Swiftのサンプルソースコードについて
サンプルソースコードは提供されています。
下記から入手可能です。https://github.com/GoogleCloudPlatform/ios-docs-samples
しかしながら現在のところはサンプルソースコードが存在するサービスはそれほど多くはないようです。
go言語と違って直ぐ書けます?
サービス 説明 speech Samples that demonstrate the Cloud Speech API. dialogflow Samples that demonstrate the Dialogflow API. solutions Samples that demonstrate systems built on Google Cloud Platform. screensaver A screensaver that features Google Cloud Platform products. text-to-speech Samples that demonstrate the Cloud Text to Speech API. natural-language Samples that demonstrate the Cloud Natural language 後処理
翻訳結果だけを見ると以下のようになっていました。
Do what you think is right. Because you are criticized for either. <== "あなたの心が正しいと思うことをしなさい。どっちにしたって批判されるのだから。" Those who do not move forward are moving backwards. <== "前進をしない人は、後退をしているのだ。" The past doesn't change no matter how much you regret. No matter how worried you are, what will happen to the future. Now, do your best now. <== "どんなに悔いても過去は変わらない。どれほど心配したところで未来もどうなるものでもない。いま、現在に最善を尽くすことである。" The most important decision is not what you do, but what you do. <== "最も重要な決定とは、何をするかではなく、何をしないかを決めることだ。" Life is not easy. That is interesting. <== "人生は楽ではない。そこが面白い。" If you don't give up yourself, there is no “losing” in your life. <== "自分で自分をあきらめなければ、人生に「負け」はない。"キャメルケースにするためもコードは既に作成済みですが、Google Translation APIの結果に使用できない文字が多く含まれています。
スペースや、ブランク、'や“”もあります。
これらを削除、または置換していく必要があります。
go言語では文字列置換のための関数が用意されていますので、下記のようなコードを書くことで、これらの不要な文字を削除・置換することが容易です。Goreplacer := strings.NewReplacer(" ", "_", ".", "_", "+", "_", "-", "_") keyword = replacer.Replace(keyword) replacer = strings.NewReplacer("\"", "", "?", "", "!", "", "“", "", "”", "", ":", "", "[", "", "]", "", "`", "", "'", "") keyword = replacer.Replace(keyword) replacer = strings.NewReplacer("#", "", "$", "", "%", "", "=", "", "@", "", "\\", "", "(", "", ")", "", ",", "", "/", "") keyword = replacer.Replace(keyword) replacer = strings.NewReplacer("&39;", "", "&", "", """, "") keyword = replacer.Replace(keyword) keyword = strings.Replace(keyword, ";", "", -1) keyword = convertToCamelCase(keyword)また、単純に全ての翻訳結果を採用してしまうとものすごく長い変数名になってしまうので、どこかで断ち切る必要があります。意味的に良い場所で断ち切るのは至難の技ですので、文字数で制限することにします。
しかし、単語の途中でブツ切れるのはかっこよくありません。
そこで、キャメルケースを生成する関数を手直しして下記のように変更しました。Gofunc convertToCamelCase(text string) string { if text == "" { return text } var keyword string var foundUnderScore = false for i := 0; i < len(text); i++ { letter := text[i : i+1] if letter == "_" { // ====== ここで文字数制限する ====== if i > 40 { break } foundUnderScore = true continue } if foundUnderScore { foundUnderScore = false keyword = keyword + strings.ToUpper(letter) } else { keyword = keyword + letter } } head := keyword[:1] rest := keyword[1:] keyword = strings.ToLower(head) + rest return keyword }まとめ
go言語は、C/C++に近いローレベル(いろんな意味で)の言語でありながら、ライブラリのパワーが凄まじいと思います。
単なるローカルなツール作成だけではなく、サーバサイドでも大活躍する言語ですので、Objective-Cに慣れ親しんだ‼️iOSエンジニアの方は一度試してみてはいかがでしょうか。
- 投稿日:2019-12-05T00:31:33+09:00
iOS13以降でPush通知が届かなくなってしまったので調査した件について
人生で初めてQiitaに投稿します。
めっちゃ緊張してます。
Qiitaの文字3回うち間違えました。なお、この記事の元ネタはもともと私が書いていたブログに書いていたものをリライトしたものです。
リライトしているのでこちらの方まだ読みやすいのはわかってますが、それでもほんのちょぴっとでもブログの方を覗いてもらえると嬉しいです。
https://yara-shimizu.com/2019/10/05/ios-ver13/iOS13になってPush通知が届かなくなってしまった??
タイトルの通り、iOS13にPush通知が届かなくなったので調べました。
環境
Mac Mojave 10.14.6
xcode 11(おそらく10以下でも発生する)
iOS 13.4.1
Objective-C現象
・iOS12以前は正常にPush通知が送れている
・iOS13以降でのみ通知が届かなくなった原因
これが100%原因かどうかはソースを見ないとなんとも言えないですが、私の場合はトークンを取得する次のコードの「description」が原因でした。
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { //デバイストークン取得後の処理 "<" ">" " "を削除 NSString *token = [[[[deviceToken description] stringByReplacingOccurrencesOfString:@"<"withString:@""] stringByReplacingOccurrencesOfString:@">" withString:@""] stringByReplacingOccurrencesOfString: @" " withString: @""]; }こんな感じでdeviceTokenを取得して、不要な文字列を削除して通知に利用するというような運用でしたが、iOS13以降ではdescriptionの返り値が変更になったようで別の方法を使わないといけないよう。
このク◯が、、、~
バージョン 返り値 iOS12以前 <00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000> iOS13以降 {length = 32, bytes = 0x00000000 00000000 00000000 00000000 … 00000000 00000000 } 解決方法
descriptionがいけないということで、関数を自作して対応をします。
天下のFacebook様も同じ対応をしています。
参考:https://forums.developer.apple.com/thread/117545- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSString *token = [self hexadecimalStringFromData:deviceToken]; } - (NSString *)hexadecimalStringFromData:(NSData *)data { NSUInteger dataLength = data.length; if (dataLength == 0) { return nil; } const unsigned char *dataBuffer = data.bytes; NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)]; for (int i = 0; i < dataLength; ++i) { [hexString appendFormat:@"%02x", dataBuffer[i]]; } return [hexString copy]; }最後に
初めてQiitaで記事をかきましたが、サイトのデザインがきれいなので中身が私みたいにゴミでもなんだかそれっぽくなりますね。
またxcode等で困った出来事があったらQiitaに書くかもです。
なにか間違い等の指摘がございましたらコメントくださると嬉しいです。
読んでくださってありがとうございました。