- 投稿日:2019-12-13T18:38:05+09:00
fastlaneでまず始めに使いたいアクション
はじめに
本記事はVolare Advent Calendar 2019の3記事目として書いています。
私は今年の4月から社会人として、主にiOSアプリの開発を行っていますが、今回はVolare卒業1期生(?)として寄稿させていただきます!fastlaneの導入方法やリリースの自動化などについては、既にたくさんの記事があるので、
今回はアプリの個人開発を行っている方や、今までfastlaneを知らなかった方でも恩恵を受けられそうな、3つのfastlaneのアクションについて、その挙動や実行方法などをまとめたいと思います。fastlaneとは
fastlaneは、iOS及びAndroidアプリのビルドやリリースを自動化してくれる、ビルドツールです。
無料で使用することができ、OSSでドキュメントも整備されているため、手軽に導入できます。
無料です。Fastfileにlaneと呼ばれるアクションの組み合わせを記述し、CUI上やCIツールなどから実行することで、様々な処理を自動化することができます。
今回はこのアクションについて、特に便利でよく使いそうなアクションを挙げていきたいと思います。アクション
gym
(引用:https://docs.fastlane.tools/actions/gym/)
![]()
![]()
![]()
gymアクションは、アプリのビルドと、ストア申請やTestFlightへのアップロードのために必要な、ipaファイルの作成を行えます。
これ、XCodeからやるとめちゃくちゃ時間かかりますよね…パラメータ
gymアクションには公式ドキュメントにあるように、様々なパラメータを渡すことができます。
私は主に、
キー名 役割 設定値 scheme アプリのスキームを指定する "[アプリ名]" configuration アプリをビルドする際の構成を指定する "Release" or "Debug" outpu-name ipaファイルの書き出し名を指定する "[アプリ名].ipa" の3つを指定しています。
gym( scheme: "MyApp", configuration: "Release", output_name: "MyApp.ipa" )このような感じですね。
pilot
(引用: https://docs.fastlane.tools/actions/pilot/)
![]()
![]()
![]()
pilotアクションは、AppleのTestFlightにアプリをアップロードしたり、テスターに配布したり、テスターの管理などができます。
テスターの追加・削除
テスターを追加したい場合は、CUI上で
fastlane pilot add [追加したいテスターのemailアドレス]と打つことで実行可能です。同様に削除したい場合も、
fastlane pilot remove [削除したいテスターのemailアドレス]のコマンドを実行するだけで、対象のテスターを綺麗サッパリ消すことが可能です。
ベータ版の配布
パラメータでipaファイルを指定し、pilotアクションを実行するだけでTestFlightへのアップロードまで行えます。
pilot( ipa: "./MyApp.ipa" )deliver
(引用:https://docs.fastlane.tools/actions/deliver/)
![]()
![]()
← UberEats
deliverアクションはアプリのリリースを自動化してくれるアクションです。XCodeを使わずに、新しい.ipaファイルをApp Store Connectにアップロードすることが可能です。
その際にprefetch機能が実行されることで、アプリがリジェクトされる危険性を含んでいないかを、事前にある程度確認もしてくれます。関連ファイル
App Storeへのアプリの提出には、スクリーンショットやリリースノート、アプリの説明文など様々な要素が必要となります。
そしてfastlaneは、これらのファイルもまとめて管理することができます。
具体的には、
./fastlane/screenshots
以下でスクリーンショットを
./fastlane/metadata/ja/release_notes.txt
でリリースノートを
./fastlane/metadata/ja/description.txt
でアプリ概要の文章をそれぞれ変更、修正できます。パラメータ
他のアクションと同様に、deliverアクションにも様々なパラメータを渡すことができます。
特に使用頻度の高そうなものとしては、
キー名 役割 設定値 force アプリ申請時のHTMLレポートファイルの検証をスキップ true submit_for_review アプリのアップロード後、審査用に新しい新しいバージョンを送信 true automatic_release アプリの審査通過後、自動的にリリースする true 等が挙げられます。実際に書くと、
deliver( force: true, submit_for_review: true, automatic_release: true )のような形ですね。
いざ実行
実際にこれらのアクションを使用する場合、CUI上で1つずつコマンドを叩いてもよいのですが、多くの場合Fastfileにレーンを作って、実行します。
レーンとは、複数のアクションを組み合わせたものです。
lane [:レーンの名前] do
とend
の間に実行したいアクションを書いていきます。具体例を示すと、
Fastfile.lane :testFlight do # ビルド gym( scheme: "MyApp", configuration: "Release", output_name: "MyApp.ipa", ) # TestFlightへアップロード pilot( ipa: "./MyApp.ipa" ) end lane :release do # ビルド gym( scheme: "MyApp", configuration: "Release", output_name: "MyApp.ipa", ) # App Store Connectへアプリ提出, リリース deliver( force: true, submit_for_review: true, automatic_release: true ) endのような形になります。
あとは、ベータ版の配布を行う場合は
bundle exec fastlane testFlight
アプリのリリースを行う場合は
bundle exec fastlane release
のように、作成したレーンを指定したコマンドをCUI上で実行すれば、自動化が行なえます。おわりに
今回はfastlaneでまず始めに使いたいアクションとして、
- gym
- pilot
- deliver
の3つを挙げ、それぞれのアクションの動作と実行方法を軽くまとめてみました。fastlaneにはこれらのアクションの他にも、証明書やプロファイルを生成、同期、管理できるmatch、pem、certといったアクションや、テストを自動化できるscan、スクリーンショットを自動生成することができるsnapshotなど、まだまだたくさんのアクションが用意されています。
この記事をきっかけにアプリ開発の自動化について興味を持ってくださった方は、ぜひ公式ドキュメントなどを参考に、他のアクションも使ってみてください!
参考
fastlane公式
fastlaneを導入する手順について
fastlaneを使ってみる
これから始めるfastlane
- 投稿日:2019-12-13T18:16:03+09:00
そのアプリ…文字サイズをデカくしてもUI崩れないですか?
はじめに
Flutter #2 Advent Calendar 2019の13日目の記事です。
株式会社TenxiaのCEOの岡部(Twitter)です。
この記事の内容はFlutter Meetup Tokyo #13の自身のLTを記事に起こしたものです。
これはなに
- 高齢化社会日本
- 2020年 女性の過半数が50歳以上になる
- 2025年 国民の1/3が高齢者になる
- 2065年 国民の2.5人に1人が高齢者に
- 今やスマホアプリは若者だけでなく全ての年齢層が使うもの
つまり
- 日本市場向けアプリを作るからには大きい文字サイズは意識したほうが良い
- エンジニア・デザイナーは若いので、自分の基準で小さすぎる文字サイズを設定しがち…
- 一方で、大きい文字サイズを嫌がるユーザも存在する
であるからして
- 文字サイズを固定にするのでなく、OSなりアプリなりで制御できるようにするべき
ということは
- 幅広い文字サイズで表示可能で、なおかつ崩れないUIが必須!
ところで
- Flutterはめちゃくちゃ簡単に(10分ぐらいで)文字サイズ調整対応ができるのでトライしてみよう
文字サイズの変更を許容した場合に発生する要件
- 変なところで改行しないか?
- 機能的に文字サイズが変化すべきでないWidgetではないか?
- 横に配置したプロフィール画像が変な位置に表示されないか?
- 省略すべきでないテキストが省略されないか?
- 大きいデバイスだと大丈夫だけど小さいデバイスで潰れたりしないか?
下の画像はTwitterのiOSアプリですが、
- プロフィールアイコンは文字サイズに合わせて拡大している
- RT数やlike数は拡大していない
- 時間は省略されない
など、前述の要件が考慮されているのが分かります。
文字サイズ最小 文字サイズ最大 今回作ったもの
Tenxia-inc/font_size_changeable
しくみ
- FlutterにはFontSizeとは別に、TextScaleFactorというpropertyがあります
- 全てのTextに作用する乗数(たとえば2.0なら、全てのテキストが2倍のサイズになる)
- OSのフォントサイズも、デフォルトではここに作用している
MediaQuery.of(context).textScaleFactor
で取得できる- また、各WidgetにもTextScaleFactorを設定でき、そちらが優先されるので、マクロのTextScaleFactorを使いたくないときは明示的に記載すれば良い
- MaterialAppにはbuilderという引数があり、そこでMediaQueryでラップしたWidgetを返すことで、アプリ全体のTextScaleFactorを設定することができる
- ここではplatformの設定とアプリ内の文字サイズを乗算し、一定の範囲内に絞った上で利用
builder: (context, snapshot) => MaterialApp( title: 'Font Size Changeable', home: const FontSizeSettingPage(), builder: (BuildContext context, Widget child) { final platformFactor = MediaQuery.of(context).textScaleFactor ?? 1; final multipliedFactor = platformFactor * snapshot.data; final adjustedFactor = min(max(_minFactor, multipliedFactor), _maxFactor); logger.info('Set textScaleFactor: $adjustedFactor'); return MediaQuery( data: MediaQuery.of(context) .copyWith(textScaleFactor: adjustedFactor), child: child, ); },という前提の下、3種類のText Widgetを使用しています。
通常のText
- 普通にText Widgetを表示してあげると、そのまま文字サイズが変更されます。
- 改行位置を気にしない、メインのコンテンツやユーザコメントなどに。
const Text( '普通に文字サイズが変更されてほしいテキスト。\n' 'メインのコンテンツとかですね', ),文字サイズ不変のText
- 一方、TextのtextScaleFactorに固定値を設定してあげると、文字サイズが変化しないWidgetになります。
- 各種ボタンや、一部のラベルなど、文字サイズが変更してほしくない場合に。
const Text( '不変であってほしいテキスト。', textScaleFactor: 1, textAlign: TextAlign.right, ),AutoSizeText
- AutoSizeTextという、与えられたサイズに応じて文字サイズを自動で変更してくれるライブラリがあり、一定以上は大きくならないWidgetを作れます。
- 説明文や注釈など、変なところで改行してほしくない場合に。
?自分は改行文字の個数に応じて自動でmaxLinesを判定してくれるWidgetを自作して使っています。
@immutable class FitText extends StatelessWidget { final String text; final int maxLines; final EdgeInsets padding; final TextStyle style; final TextAlign textAlign; const FitText( this.text, { this.maxLines, this.padding = EdgeInsets.zero, this.style, this.textAlign, Key key, }) : super(key: key); @override Widget build(BuildContext context) { final lines = maxLines ?? text.split('\n').length; return Padding( padding: padding, child: AutoSizeText( text, textAlign: textAlign, maxLines: lines, style: style, minFontSize: 1, ), ); } }TextWidget以外
プロフィールアイコンなど、比例して大きくなるべきWidgetも、
SizeにMediaQuery.of(context).textScaleFactor
を乗算してやればいい感じになります。まとめ
- という風に、Flutterでは簡単に文字サイズ変更に対応できます。
- 各ネイティブでこれを実現しようと思うとめちゃくちゃ大変なはず。
- Flutter歴2ヶ月の新参者ですが、Flutter、かなりメインストリームになってきていると思います。
- 会社でもう一記事、このAdvent Calenderに寄稿してるので、よろしければどうぞ。
- 良いFlutter Lifeを!
- 投稿日:2019-12-13T16:50:56+09:00
[UE4] UnrealEngine4の開発用の2019 16' MacBook Proのレポート
UE4のために新16インチMacBookPro(2019)を購入しました。
今までWindowsで作業して、iOSやtvOSなどのプロジェクトをパッケージするときのみ2017のiMacを使用していました。
EGJでiOSの仕事が増えて、MacOSだけでUE4作業できるマシンが欲しいという話になりました。
現在のラインアップで、マックミニが専用GPUが付いていないのでエディターの作業が辛い。
iMac Proが良いですが、フルスペックにすると値段がかなり上がって、二年前からスペックが変わっていなくて、更新される前に買うことが良いと思えない状態です。
今週から買えるMac Proが非常に良さそうですが、値段も非常に良いので、
改めて考えたら、フルスペック16'MBPの仕様と値段がちょうどいいかと思いました。スペック
簡単にいうと、フルスペックのモデルを注文しました。
プロセッサ 第9世代の2.4GHz 8コアIntel Core i9プロセッサ(Turbo Boost使用時最大5.0GHz)
メモリ 64GB 2,666MHz DDR4メモリ
グラフィックス AMD Radeon Pro 5500M(8GB GDDR6メモリ搭載)
ストレージ 8TB SSDストレージ
キーボード - 英語(米国)
そのスペックを合わせて、¥651,800(税別)の金額になります。CPUが15インチモデルと同じCoffee Lakeマイクロアーキテクチャですが、クーリングシステムが新しくなったおかげで、20〜30パーセントくらい速くなっています。エンジンのビルドする時に非常に嬉しいことです。Mac上で分散ビルドが未対応なので、CPUに関しては節約しようと考えない方がいいと思います。
GPUがレイトレーシングに対応していないのは残念ですが、メモリが多くてボトルネックにならなかったです。
SSDとメモリーがちょっと贅沢して入れました。このパソコンを長く使う予定であり、余裕を持って選ぼうと決めました。
同じディスクでWinのパーティションを利用するとしたら、8TBのSSDは意味があると思います。
だた予算に余裕があまりなければ、UE4の作業で、4か2TBでも十分だと思います。それだけでマシンの金額がかなり下がって、4TBの場合は¥519,800(税別)で、2TBの場合は¥453,800(税別)になります。プログラミングとUE4用なので、キーボードを米国にしました。スラックなどに日本語で書くと、きっと大変だろうと思いましたが、嬉しいことにそうでもないです。これについて感想で改めて書きます。
パフォーマンス
テスト環境 :
MacOS 10.15.1
Xcode 11.3
UE4.23.1 (ソース版)
充電器を刺したまま、気温24度の部屋でテストを行いました。CPU
Engine
Xcodeでエンジンのフルクリーンビルドをしてみて、36分58秒で作成できました。分散ビルドせずに40分切っていることは、すごいと思います。
熱が82度と95度の間でした。ActionRPG
Mac版のパッケージを作って、Deployして、シェーダーコンパイルも含めて、
13分04秒でできました。熱は85度を超えませんでした。PIEと1080pのパッケージのどっちも、一番遅いスレッドはDrawでしたが、15ミリを超えなかったので、心配はいらないと思います。コンソールかPCのAAAクオリティのゲーム開発だと話が変わりますが、iOS、tvOSの高クオリティのゲームを作るとしてもGPUが心配いらないと思います。
GPU
MegaScansのMeadowPackのShowcaseを使って、
レベルの最初のロードのせいでエディタのフレームレートがかなり下がります。CPUとメモリにも原因があります。
準備が終わったら、GPUが一回も16ミリを超えませんでした。
PIEでも問題なく、そしてパッケージ版も1080pで綺麗な60FPSが出ました。バッテリー
一回エンジンのフルリコンパイルを行って、サーマルスロットリング(省電力モード)があると確認できました。
熱は75度を超えなくて、45分48秒でビルドできました。
バッテリーが95%から23%まで減りました。日常の作業でバッテリーでフルリコンパイルをやることとしてはおすすめできないですが、バッテリーだけでフルビルドをできるノートパソコンを初めて見ました。Bluetoothイヤホンで音楽を聴きながらネット、UE4、iPhoneまでのDeploy、Xcodeでコード書いてプチコンパイルするなど普通の作業の場合は、2時間半くらい持ちました。個人的に大満足です。
感想
ディスプレイ
画面が大きくて見やすいです。弊社のテクニカルアーティストも「いいね!」と言いました。
音とマイク
音が凄く良くて、パッケージ版のサウンド実装を確認するときにちょうどいいと思います。
マイクのクオリティが高くて、リモート会議するときにヘッドフォンのマイクを利用しなくてもいいと思いました。おまけ
- TouchBarが付いているマックを初めて使いました。良さがなかなか分からないと思っていましたが、言語の切替ボタンを追加可能で、控えめに言って、神機能だと思います。 今までよく米国のキーボードで日本語を書くことがあって、WindowsでもMacOSでも大変でしたが、初めて、全然問題ない!と思いました。 タッチバーの他の機能をあまり使わないですが、そのオプションだけで、タッチバーには十分価値があると思いました。 主にプログラミングのために購入するマシンだったら、米国のキーボードを推奨します。そうするとUE4エディタとPIEのキーボードレイアウトのトラブルも減ります。 タッチバーのおかげで一瞬で言語を切り替えて、そして現在の言語がタッチバーのアイコンで表示されているので、日本語で入力しても安心です。
iPadがMacの2台目のディスプレイになる、最近のOSに追加されたSideCarという機能を使ってみて、iPadでXcodeを表示して、Macのディスプレイでエディタの画面にして、ノートパソコンでこんなに楽なワークフローも作れるのかと思いました。どこでも持っていけるワークステーションになる感じです。
好みですが、UE4とプログラミングで、Appleのマジックマウスをおすすめできませんが、ロジクールのMX Master3が凄くいいと思いました。完璧にMacOSに対応しています。
まとめ
UE4が普通に動いています。ノートパソコンなのに、デスクトップと比較できるパフォーマンスが出ています。
エディタが安定していて、この二週間で一回も落ちなかったです。
これからiOSとtvOSのゲームを作る人にお勧めします。
Windowsで開発してMacでビルドさせるのではなく、Macで開発自体も出来るんだ!!と思いました。簡単にまとめると、
高いけどよき。
- 投稿日:2019-12-13T11:40:40+09:00
Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/gulp/node_modules/fsevents/.node-gyp'
appium/appium-ios-deviceの導入でつまづいた内容のまとめです。
環境
MacBookPro15 Mojave - macOS 10.14.6
事象
gulp
が無いと言われたのでインスコへ$ npm run watch > appium-ios-device@1.2.1 watch /Users/gremito/git/appium-ios-device > gulp watch sh: gulp: command not found npm ERR! code ELIFECYCLE npm ERR! syscall spawn npm ERR! file sh npm ERR! errno ENOENT npm ERR! appium-ios-device@1.2.1 watch: `gulp watch` npm ERR! spawn ENOENT npm ERR! npm ERR! Failed at the appium-ios-device@1.2.1 watch script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm WARN Local package.json exists, but node_modules missing, did you mean to install? npm ERR! A complete log of this run can be found in: npm ERR! /Users/gremito/.npm/_logs/2019-12-13T01_50_49_622Z-debug.log
gulp
インストールすると
permssion denied
でエラーになりました。$ sudo npm install gulp -g Password: npm WARN deprecated fsevents@1.2.9: One of your dependencies needs to upgrade to fsevents v2: 1) Proper nodejs v10+ support 2) No more fetching binaries from AWS, smaller package size /usr/local/bin/gulp -> /usr/local/lib/node_modules/gulp/bin/gulp.js > fsevents@1.2.9 install /usr/local/lib/node_modules/gulp/node_modules/fsevents > node install node-pre-gyp WARN Using needle for node-pre-gyp https download node-pre-gyp WARN Pre-built binaries not installable for fsevents@1.2.9 and node@12.13.1 (node-v72 ABI, unknown) (falling back to source compile with node-gyp) node-pre-gyp WARN Hit error EACCES: permission denied, mkdir '/usr/local/lib/node_modules/gulp/node_modules/fsevents/lib' gyp WARN EACCES current user ("nobody") does not have permission to access the dev dir "/Users/gremito/Library/Caches/node-gyp/12.13.1" gyp WARN EACCES attempting to reinstall using temporary dev dir "/usr/local/lib/node_modules/gulp/node_modules/fsevents/.node-gyp" gyp WARN install got an error, rolling back install gyp WARN install got an error, rolling back install gyp ERR! configure error gyp ERR! stack Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/gulp/node_modules/fsevents/.node-gyp' gyp ERR! System Darwin 18.7.0 gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "configure" "--fallback-to-build" "--module=/usr/local/lib/node_modules/gulp/node_modules/fsevents/lib/binding/Release/node-v72-darwin-x64/fse.node" "--module_name=fse" "--module_path=/usr/local/lib/node_modules/gulp/node_modules/fsevents/lib/binding/Release/node-v72-darwin-x64" "--napi_version=5" "--node_abi_napi=napi" "--napi_build_version=0" "--node_napi_label=node-v72" gyp ERR! cwd /usr/local/lib/node_modules/gulp/node_modules/fsevents gyp ERR! node -v v12.13.1 gyp ERR! node-gyp -v v5.0.5 gyp ERR! not ok node-pre-gyp ERR! build error node-pre-gyp ERR! stack Error: Failed to execute '/usr/local/bin/node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js configure --fallback-to-build --module=/usr/local/lib/node_modules/gulp/node_modules/fsevents/lib/binding/Release/node-v72-darwin-x64/fse.node --module_name=fse --module_path=/usr/local/lib/node_modules/gulp/node_modules/fsevents/lib/binding/Release/node-v72-darwin-x64 --napi_version=5 --node_abi_napi=napi --napi_build_version=0 --node_napi_label=node-v72' (1) node-pre-gyp ERR! stack at ChildProcess.<anonymous> (/usr/local/lib/node_modules/gulp/node_modules/fsevents/node_modules/node-pre-gyp/lib/util/compile.js:83:29) node-pre-gyp ERR! stack at ChildProcess.emit (events.js:210:5) node-pre-gyp ERR! stack at maybeClose (internal/child_process.js:1021:16) node-pre-gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:283:5) node-pre-gyp ERR! System Darwin 18.7.0 node-pre-gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/gulp/node_modules/fsevents/node_modules/node-pre-gyp/bin/node-pre-gyp" "install" "--fallback-to-build" node-pre-gyp ERR! cwd /usr/local/lib/node_modules/gulp/node_modules/fsevents node-pre-gyp ERR! node -v v12.13.1 node-pre-gyp ERR! node-pre-gyp -v v0.12.0 node-pre-gyp ERR! not ok Failed to execute '/usr/local/bin/node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js configure --fallback-to-build --module=/usr/local/lib/node_modules/gulp/node_modules/fsevents/lib/binding/Release/node-v72-darwin-x64/fse.node --module_name=fse --module_path=/usr/local/lib/node_modules/gulp/node_modules/fsevents/lib/binding/Release/node-v72-darwin-x64 --napi_version=5 --node_abi_napi=napi --napi_build_version=0 --node_napi_label=node-v72' (1) npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules/gulp/node_modules/fsevents): npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 install: `node install` npm WARN optional SKIPPING OPTIONAL DEPENDENCY: Exit status 1 + gulp@4.0.2 added 316 packages from 217 contributors in 8.129s解決方法
エラー内容をググるとVuejs界隈でも似たような現象が起きていたようです。
Errors during install of vue-cli on Mac OSX #3402を参考に再度gulpをインストールすると正常に入りました。$ sudo npm install -g gulp --unsafe-perm npm WARN deprecated fsevents@1.2.9: One of your dependencies needs to upgrade to fsevents v2: 1) Proper nodejs v10+ support 2) No more fetching binaries from AWS, smaller package size /usr/local/bin/gulp -> /usr/local/lib/node_modules/gulp/bin/gulp.js > fsevents@1.2.9 install /usr/local/lib/node_modules/gulp/node_modules/fsevents > node install node-pre-gyp WARN Using needle for node-pre-gyp https download [fsevents] Success: "/usr/local/lib/node_modules/gulp/node_modules/fsevents/lib/binding/Release/node-v72-darwin-x64/fse.node" is installed via remote + gulp@4.0.2 added 68 packages from 33 contributors and updated 1 package in 5.836s
- 投稿日:2019-12-13T09:33:36+09:00
CarPlay利用中に画面を見ずにLine送信したい
これは何?
CarPlay使っていますか?
スマホより不自由で、カーナビよりも「ゆるっと」していますが、なんとか使えています。
地図や音楽は選択肢が増えましたが、メッセージ関係は限定的なのが不満です。本記事はCarPlay中にLineにメッセージを送信する方法です。
家族に「渋滞で遅れるよ」などと簡単なメッセージを送るために作りました。IFTTTとiOSのショートカットアプリを連携して実現しています。
出来ること
- 音声操作だけで、画面を一切見ずにLine送信ができます。
送信先(Lineグループ)は固定です。複数のショートカットとIFTTTレシピを作成すれば、複数の送り先に対応可能となるでしょう。
一息で喋れる文章を送信します。息継ぎすると、そこでSiriが勝手に締め切ります。
Lineの受信に関しては、ノーケアです。安全に停車してLineアプリを使いましょう。
不具合または制限
- 送信先は、個人宛でもLineグループが必要です(自分、相手、LINE Notifyの3人)
- CarPlayから利用すると、たまにショートカットのスクリプト実行が途中で終了してしまいます。その場合は、最初からやり直しとなります。原因不明です。
- 音声入力の直前や終了時に、カスタムできない固定の文章を喋るので、Siriの返事がくどいです。例えばこんな感じです。
- 「送信しますか?」(自分が作った文章)
- 「テキストはなんですか?」(文章聞き取りの際、勝手に挿入される文章)
必要なもの
- CarPlay対応車両
- CarPlay対応iPhone
- IFTTTアカウント
- Lineアカウント
- ショートカットアプリ
事前準備
webhook ifttt line
でググってLineに送信できるURLを作成してください。
こんなのですhttps://maker.ifttt.com/trigger/イベント名/with/key/キー文字列
IFTTTでLineのレシピを作成する時点で、送信先と文章を設定しまので、送り先は固定となります。
IFTTTのMessage設定はValue1
だけ残して、残りは消しておきます。
このValue1
は、ただの文字列ではなく、ingredientの方です。ショートカット作成
事前準備ができたら、iPhoneのショートカットアプリを起動して、「+ショートカットを作成」します。
ショートカットの内容
ショートカットが起動したことを知らせます。
利用している機能「テキストを読み上げる」
まずは送信する文章を音声入力で取得します。念の為、文章を復唱します。
利用している機能「テキストを音声入力」「テキストを読み上げる」「変数を設定」
「テキストを音声入力」については、全て表示を増やす→聞き取りを停止=タップ時としてください。
次に復唱した文章でよいかを確認します。
利用している機能「テキストを読み上げる」
IF文にします。「はい」「いいえ」で答えることを想定しています。
利用している機能「テキストを音声入力」「if文」
IF文の成立時の内容
IFTTTを経由してLine送信します。
URLには、事前準備で取得したIFTTTのwebhookアドレスを設定します。
利用している機能「URL」「URLの内容を取得」
送信が完了したことを喋ります。くどい感じの反応になりますが、全部の処理が完了せずに処理が終わってしまうことがあるので、正常終了の確認用です。
利用している機能「テキストを読み上げる」
IF文の成立時の処理は以上です。IF文の不成立時の内容
送信を中断したことをお知らせします。
利用している機能「テキストを読み上げる」
ここまでできたら、画面右上の「次へ」をタップして保存します。
ショートカット名を決める
ショートカットの名前はSiriから呼び出す際に、自分が喋る言葉になります。
他のアプリとカブると、ショートカットよりもアプリが優先的に呼び出されることがあります。
自分は覚えやすさを優先して「ライン送信」としました。
そうするとLineアプリが出てきてしまうので、LineのSiri連携を切っています。
(それでもたまにLineアプリが起動してしまいます)使い方
ハンドルのSiri起動ボタンを押して、ショートカット名を喋ります。
スマホ単体でSiriを起動しても同じことができますので、デバッグや練習はCarPlayは不要です。
CarPlay中は、何故かショートカットが最後まで実行されないことがあります。
そういう場合は、もう一度Siri起動ボタンを押すところから始めましょう。
- 投稿日:2019-12-13T08:17:26+09:00
JSによる様々なSafari ITP影響ブラウザからのアクセス判定の方法
ITP対応のため、各アドテクノロジーツール提供元企業がJavaScriptを使ってどのようにSafariブラウザからのアクセスを判定しているのか個人的に気になったのでまとめました。参考までにどうぞ。
SafariブラウザのUser Agent例
デバイス OS User Agent iPhone Safari 13 on iOS 13 Mozilla/5.0 (iPhone; CPU iPhone OS 13_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1
iPad Safari 12.1 on iOS 12.2 Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1
mac Safari 12 on macOS (High Sierra) Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Safari/605.1.15
その他の例は以下記事を参照
- 各ブラウザのUserAgent一覧 - Qiita
- iOS 13 / iPadOS 13 Webブラウザのユーザーエージェント (Safari, Google Chrome, LINE, Twitter など) - Qiita
- Safari User Agents - WhatIsMyBrowser.com
各アドテクツール内での判定
Yahoo! JAPAN
サイトジェネラルタグ内にてシンプルな正規表現によりUserAgentの文字列を見てSafariかどうか推定。
UserAgentに
iPhone
かiPad
かMacintosh
かいずれかの文字列を含んでおり、
かつ、それ以降に、Version/
を含み、その直後に11~19の数字が続き、
かつ、それ以降にSafari/
という文字列を確認できた場合、ITP影響下にあるSafariブラウザであると推定。
https://s.yimg.jp/images/listing/tool/cv/ytag.jsisSafari: function(e) { return new RegExp("(iPhone|iPad|Macintosh).*Version/1[1-9].*Safari/").test(e) }Google Analytics
UserAgentに
"Cheome"
,"crios"
(iOS版Chrome)の何れの文字列も含まない。
かつ、"Safari/"
か"Safari,"
の文字列を含むとき、Safariブラウザからのアクセスと推定。バージョン11以上でITP影響ブラウザと判定。
https://www.google-analytics.com/analytics.jsvar oc, Id = /^.*Version\/?(\d+)[^\d].*$/i, ne = function() { if (void 0 !== O.__ga4__) return O.__ga4__; if (void 0 === oc) { var a = O.navigator.userAgent; if (a) { var b = a; try { b = decodeURIComponent(a) } catch (c) {} if (a = !(0 <= b.indexOf("Chrome")) && !(0 <= b.indexOf("CriOS")) && (0 <= b.indexOf("Safari/") || 0 <= b.indexOf("Safari,"))) b = Id.exec(b), a = 11 <= (b ? Number(b[1]) : -1); oc = a } else oc = !1 } return oc };Criteo
以下の正規表現とマッチするときSafariブラウザと推定。更にバージョン11以上でITP影響ブラウザと判定。
/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[^ ]+ \(KHTML, like Gecko\) Version\/([^ ]+)( Mobile\/[^ ]+)? Safari\/[^ ]+$/i
https://static.criteo.net/js/ld/ld.jsLogicad
UserAgentを小文字化した上で、
"iphone"
,"ipod"
,"ipad"
の何れかの文字列を含んでいることを確認し、
その上で、"crios"
(iOS版Chrome),"opera"
,"opios"
(iOS版Opera)などの文字列を含んでいないことを確認できたとき
Safariブラウザからのアクセスと推定。Macの存在は無視している?
https://cd.ladsp.com/script-sf/v5/sf.min.jssmnLogicadSfManager.isIosSafari = function () { var u = window.navigator.userAgent.toLowerCase(), c = function (s) { return -1 != u.indexOf(s) }, d = function (s) { return !(function (s) { return -1 != u.indexOf(s) })(); }; return (c("iphone") || c("ipod") || c("ipad")) && d("crios") && d("opera") && d("opios") && d("fxios") && d("google") && d("yahoo") && d("y!j") && d("bing") && d("bot") && d("crawl") && d("spider") };MicroAd BLADE
UserAgentに
Safari
を含んでおり、かつVersion/
の後に何かしらの1文字があり、その後に数字が続いた場合、
かつ、Version/
の後に続く数字が11以上ならITP影響ブラウザと判定。(「何かしらの1文字」の判定は何のために用意してるんだろう。。)
https://d-cache.microad.jp/js/blade_track_jp.jsfunction isItpBrowser() { if (navigator.userAgent.match(/Safari/) && navigator.userAgent.match(/Version\/(.\d+)/)) { return navigator.userAgent.match(/Version\/(.\d+)/)[1] >= 11; } else { return false; } }AccessTrade
UserAgentを小文字化した上で
"macintosh"
,"iphone"
,"ipad"
の何れかの文字列を含んでいる。
かつ、"safari"
,"version"
の文字列を両方含み、"chrome"
,"edge"
の何れの文字列7含まないとき、Safariと推定。
更に、UserAgent中のos
あるいはmac os x 10_
の直後の2桁の数字を見ることでITP影響ブラウザと判定。
https://h.accesstrade.net/js/nct/lp.min.jst.hasITP = function(e) { var t = e.toLowerCase(); if ((-1 !== t.indexOf("macintosh") || -1 !== t.indexOf("iphone") || -1 !== t.indexOf("ipad")) && -1 !== t.indexOf("safari") && -1 !== t.indexOf("version") && -1 === t.indexOf("chrome") && -1 === t.indexOf("edge")) { var n = t.match(/os (\d{2})_/); if (null !== n && +n[1] >= 11) return !0; var r = t.match(/mac os x 10_(\d{2})/); if (null !== r && +r[1] >= 13) return !0 } return !1 }部分部分を切り取って見ているのでもっとよく読んだら更にしっかりした判定をしているかも。各社個性があって面白いです。
他にも見つけたら随時追加予定。その他参考になる記事
- 投稿日:2019-12-13T08:17:26+09:00
JSを使った様々なSafariブラウザからのアクセス判定の方法
ITP対応のため、各アドテクノロジーツール提供元企業がJavaScriptを使ってどのようにSafariブラウザからのアクセスを判定しているのか個人的に気になったのでまとめました。参考までにどうぞ。
SafariブラウザのUser Agent例
デバイス OS User Agent iPhone Safari 13 on iOS 13 Mozilla/5.0 (iPhone; CPU iPhone OS 13_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1
iPad Safari 12.1 on iOS 12.2 Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1
mac Safari 12 on macOS (High Sierra) Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Safari/605.1.15
その他の例は以下記事を参照
- 各ブラウザのUserAgent一覧 - Qiita
- iOS 13 / iPadOS 13 Webブラウザのユーザーエージェント (Safari, Google Chrome, LINE, Twitter など) - Qiita
- Safari User Agents - WhatIsMyBrowser.com
各アドテクツール内での判定
Yahoo! JAPAN
サイトジェネラルタグ内にてシンプルな正規表現によりUserAgentの文字列を見てSafariかどうか推定。
UserAgentに
iPhone
かiPad
かMacintosh
かいずれかの文字列を含んでおり、
かつ、それ以降に、Version/
を含み、その直後に11~19の数字が続き、
かつ、それ以降にSafari/
という文字列を確認できた場合、ITP影響下にあるSafariブラウザであると推定。
https://s.yimg.jp/images/listing/tool/cv/ytag.jsisSafari: function(e) { return new RegExp("(iPhone|iPad|Macintosh).*Version/1[1-9].*Safari/").test(e) }Google Analytics
UserAgentに
"Cheome"
,"crios"
(iOS版Chrome)の何れの文字列も含まない。
かつ、"Safari/"
か"Safari,"
の文字列を含むとき、Safariブラウザからのアクセスと推定。バージョン11以上でITP影響ブラウザと判定。
https://www.google-analytics.com/analytics.jsvar oc, Id = /^.*Version\/?(\d+)[^\d].*$/i, ne = function() { if (void 0 !== O.__ga4__) return O.__ga4__; if (void 0 === oc) { var a = O.navigator.userAgent; if (a) { var b = a; try { b = decodeURIComponent(a) } catch (c) {} if (a = !(0 <= b.indexOf("Chrome")) && !(0 <= b.indexOf("CriOS")) && (0 <= b.indexOf("Safari/") || 0 <= b.indexOf("Safari,"))) b = Id.exec(b), a = 11 <= (b ? Number(b[1]) : -1); oc = a } else oc = !1 } return oc };Criteo
以下の正規表現とマッチするときSafariブラウザと推定。更にバージョン11以上でITP影響ブラウザと判定。
/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[^ ]+ \(KHTML, like Gecko\) Version\/([^ ]+)( Mobile\/[^ ]+)? Safari\/[^ ]+$/i
https://static.criteo.net/js/ld/ld.jsLogicad
UserAgentを小文字化した上で、
"iphone"
,"ipod"
,"ipad"
の何れかの文字列を含んでいることを確認し、
その上で、"crios"
(iOS版Chrome),"opera"
,"opios"
(iOS版Opera)などの文字列を含んでいないことを確認できたとき
Safariブラウザからのアクセスと推定。Macの存在は無視している?
https://cd.ladsp.com/script-sf/v5/sf.min.jssmnLogicadSfManager.isIosSafari = function () { var u = window.navigator.userAgent.toLowerCase(), c = function (s) { return -1 != u.indexOf(s) }, d = function (s) { return !(function (s) { return -1 != u.indexOf(s) })(); }; return (c("iphone") || c("ipod") || c("ipad")) && d("crios") && d("opera") && d("opios") && d("fxios") && d("google") && d("yahoo") && d("y!j") && d("bing") && d("bot") && d("crawl") && d("spider") };MicroAd BLADE
UserAgentに
Safari
を含んでおり、かつVersion/
の後に何かしらの1文字があり、その後に数字が続いた場合、
かつ、Version/
の後に続く数字が11以上ならITP影響ブラウザと判定。(「何かしらの1文字」の判定は何のために用意してるんだろう。。)
https://d-cache.microad.jp/js/blade_track_jp.jsfunction isItpBrowser() { if (navigator.userAgent.match(/Safari/) && navigator.userAgent.match(/Version\/(.\d+)/)) { return navigator.userAgent.match(/Version\/(.\d+)/)[1] >= 11; } else { return false; } }AccessTrade
UserAgentを小文字化した上で
"macintosh"
,"iphone"
,"ipad"
の何れかの文字列を含んでいる。
かつ、"safari"
,"version"
の文字列を両方含み、"chrome"
,"edge"
の何れの文字列7含まないとき、Safariと推定。
更に、UserAgent中のos
あるいはmac os x 10_
の直後の2桁の数字を見ることでITP影響ブラウザと判定。
https://h.accesstrade.net/js/nct/lp.min.jst.hasITP = function(e) { var t = e.toLowerCase(); if ((-1 !== t.indexOf("macintosh") || -1 !== t.indexOf("iphone") || -1 !== t.indexOf("ipad")) && -1 !== t.indexOf("safari") && -1 !== t.indexOf("version") && -1 === t.indexOf("chrome") && -1 === t.indexOf("edge")) { var n = t.match(/os (\d{2})_/); if (null !== n && +n[1] >= 11) return !0; var r = t.match(/mac os x 10_(\d{2})/); if (null !== r && +r[1] >= 13) return !0 } return !1 }部分部分を切り取って見ているのでもっとよく読んだら更にしっかりした判定をしているかも。各社個性があって面白いです。
他にも見つけたら随時追加予定。その他参考になる記事
- 投稿日:2019-12-13T07:49:17+09:00
俺のアプリがこんなにダークなわけがない
タイトルの言い回しがもう古いかもしれませんが、最近の流行りがわからないのです・・・
こんなアプリを作ったよ
以前、続:どこまでショボいアプリがAppleの審査に通るのか試してみたで作ったアプリをiOSのダークモードに対応してみました。
このアプリは5W1Hで予言を表示するというアプリでした。このアプリを
ライトモードなら白背景
ダークモードなら黒背景にしてみました。
でも・・・ただ、それだけじゃつまらないよね・・・
ということで、ダークモードの時は「暗い奴」にすることにしました。
え、意味がわからない?
つまりこういうことです。これはライトモード。
色合いも美しく、シンプルで見とれてしまいますね。
内容もなんだかパリピな感じで明るい。#ちなみに色はmaterialize-cssのカラーパレットから選びました
#CSSフレームワークのカラーパレットから選ぶと良い感じにしやすいですさて、これをiPhoneをダークモードにして実行すると・・・
なんということでしょう!内容が陰湿になりました!
見た目も中身もまさしくダークモードです!
OSのカラーモードでアプリの機能まで変えてしまうという、機能の趣旨を無視したアプリを作ることで世に一石を投じてみました。動画で見るとこんな感じです。
ライトモードかダークモードかで言葉の辞書を変えてます。こんな実装だよ
実はこの記事はXamarinのアドベントカレンダーに参加してるのでXamarinっぽいことも書きます。
ResouceDictionaryとDynamicResourceを使って、モードに応じて色を切り替えてます。まずダークモードの判定は、Xamarin.iOS側に以下のような実装を書くことで取得できます。
//iOS側の処理(DependencyServiceで取得できるようにする) public bool IsDarkMode() { if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0)) { if (new UIViewController().TraitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark) { return true; } } return false; }これをアプリの起動と再開時に取得します。
//App.xaml.cs protected override void OnStart() { SetTheme(); } protected override void OnResume() { SetTheme(); } private void SetTheme() { //1回スレッドを切らないとレジューム時に値がちゃんと取れなかった Device.BeginInvokeOnMainThread(() => { IsDark = DependencyService.Get<IDeviceService>().IsDarkMode(); if (IsDark) { Resources["Background"] = Color.FromHex("000000"); Resources["TextColor"] = Color.FromHex("FFFFFF"); Resources["Accent"] = Color.FromHex("FFFF00"); } else { Resources["Background"] = Color.FromHex("fafafa"); Resources["TextColor"] = Color.FromHex("424242"); Resources["Accent"] = Color.FromHex("1565c0"); } }); }最後のResoucesについてはApp.xaml側に書いてます。
<?xml version="1.0" encoding="utf-8"?> <Application xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Glorious.App"> <Application.Resources> <ResourceDictionary> <Color x:Key="Background">#000000</Color> <Color x:Key="TextColor">#FFFFFF</Color> <Color x:Key="Accent">#FFFF00</Color> </ResourceDictionary> </Application.Resources> </Application>このResourcesを画面側で以下のような感じで参照してます。
<Button Text="{Binding Predict.ButtonText}" WidthRequest="260" Clicked="Handle_Clicked" HorizontalOptions="Center" Margin="0,30,0,0" FontSize="20" TextColor="{DynamicResource TextColor}" ←ここ BackgroundColor="{DynamicResource Background}" ←ここ BorderColor="{DynamicResource TextColor}" ←ここ BorderWidth="1" Padding="5,0,5,0"></Button>あとはダークモードかどうかで表示する言葉の辞書を切り替えていました。
まとめ
以上、ダークモードとは何なのかを履き違えたアプリを作ってみました。
今回の内容とは全く関係ないですが、単純なアプリでも良いので1個作っておくと、こんな感じでちょっとした調査に使いまわせて良いですよ。
- 投稿日:2019-12-13T07:47:26+09:00
SwiftのExtensionをより分かりやすく -TargetedExtension-
はじめに
本記事はCyberAgent 20新卒 Advent Calendar 2019の13日目の記事です!
もうすこしでクリスマスですね??// TODO: 追記します!
- 昨日の投稿は, Ayumuくんの「Lottieを使ったAndroidのアニメーション実装」でした!
- そして, 14日目の投稿はdragon_taroくんの「」です!
ぜひこちらもご覧ください!!本日(アドベントカレンダー13日目)の記事は, よだだよ!or ヨーダが担当します!
普段から触っている技術はiOSで, 最近は研究や受託開発等でARKitを使うことが多いです.しかし,
今回はSwiftExtensionの記事を書こうと思います!※実は, 「ARKitを使って, クリスマスぽいお絵かきARアプリを作ってみよう!??」というタイトルで書こうと思ってましたが, 間に合わなそうなのでテーマを急遽変更しました!
時間のあるときにでも, お絵かきAR作ってみる記事は書こうと思います.SwiftのExtensionとは...
- クラスや構造体, 列挙体の拡張
- プロトコル拡張(デフォルト実装)
- クラスや構造体, 列挙体をプロトコルに適合させる
Extension+String.swiftextension String { // 追加するメソッド等 func printHoge() { print("hoge") } } // 呼び出し String().printHoge()↑例えばこのように拡張することはあるかと思いますが,
もとからあるのもなのか, それとも拡張され追加されたものなのかを判別するのは分からないと言う問題があります.そこで, 拡張され追加されたものには, プレフィックスを付けます.
そのようにすることによって, 拡張されたものであることが明示的になると同時に, プロパティの衝突も避けることができます.では, ここから実際にTargetedExtensionを実装していきます!
Extensions実装
今回は例として, FileManagerを拡張してみたいと思います!
ちなみに, このメソッドは指定パスにあるファイルを一覧を表示するものです.下のものは, 単純にFileManagerを拡張したものになります.
FileManager+Extensions.swiftimport Foundation public extension FileManager { /// List local files (specified path) func listupLocalFiles(path: String) { do { // Get all file name strings in path let files = try FileManager().contentsOfDirectory(atPath: path) // Get file name as String array print(files) } catch let error { // Error : If path doesn't exist print(error) } } } // Usage: Temp Directory let fm = FileManager() fm.listupLocalFiles(path: NSTemporaryDirectory())普通に拡張すると,
fm.listupLocalFiles(path: NSTemporaryDirectory())
になり,
最初に挙げたように, 元からあるものなのか, 追加されたものなのかが分かりません.Targeted Extensionsで実装
FileManager+Extensions.swiftimport Foundation public protocol FileManagerComapatible { associatedtype CompatibleType var ex: CompatibleType { get } } public final class FileManagerExtension<Base> { private let base: Base public init(_ base: Base) { self.base = base } } public extension FileManagerComapatible { var ex: FileManagerExtension<Self> { return FileManagerExtension(self) } } extension FileManager: FileManagerComapatible { } extension FileManagerExtension where Base == FileManager { /// ローカルファイル(指定パス)のリストアップ func listupLocalFiles(path: String) { do { // Get all file name strings in path let files = try FileManager().contentsOfDirectory(atPath: path) // Get file name as String array print(files) } catch let error { // Error : If path doesn't exist print(error) } } } // Usage: // let fm = FileManager() // fm.ex.listupLocalFiles(path: NSTemporaryDirectory())TargetedExtension呼ばれる形で実装することで,
fm.ex.listupLocalFiles(path: NSTemporaryDirectory())
といった形にすることができます.メソッドの前に,
ex
を挟むことで, 「これは追加されたもの」なんだと理解することができます!// TODO: コードの説明の追加
まとめ
- TargetedExtension呼ばれるにすることで, Extensionであることが明示的になる
- また, 名前の衝突も避けることができるというメリットが生まれます.
※後で, #TargetedExtension部分は, コードの説明を交えて丁寧なものにしたいと思います.
参考
- モダンなSwiftのExtensionについて - Targeted Extensions
- モダンなSwiftのExtension(TargetedExtensions)を実装するときハマったところ
- Swiftのextensionは3パターンだけ〜そして条件付き適合へ・・・〜
告知
今年もサイバーエージェントの本選考がやってきたみたいです!
興味ある方は, 以下のURLからチェックしてみてください!!
https://www.cyberagent.co.jp/careers/special/engineer2021/
- 早期選考で内定となった場合, 長期実務インターンをすることができます!
- #cypitch をつけてTwitterに呟くと, それが会社説明資料になるみたいです!
僕自身がCyberAgentを選んだ理由はたくさんありますが,
その1つとして「人の良さ」です!!
「人の良さ」ってのを言葉で正確に伝えるのは難しいので, Zehi社員さんに会ってみてください!(勿論, 内定者でも大丈夫です!)
きっとこの言葉の意味が感じられるような気がします!最後まで読んでくださり, ありがとうございました?♂️
- 投稿日:2019-12-13T07:35:57+09:00
【iOS】サンプルデータ、どうやってアプリに入れますか
はじめに
Releaseビルドには要らないけど、開発中には使いたいサンプルデータ。このサンプルデータ、どうやってアプリに入れますか?
まず、想定しているサンプルデータの使い方を以下に挙げます。
- 主にUIの開発に用いる。UIを作るためにも表示するデータがほしい
- 多言語対応したデータを用意したい。AppStoreのスクショにも使う
![]()
- 開発中は全消しして入れ直すことは多々あるので楽な方法で入れたい
- テスト用のテストデータのようにパターンを網羅したいわけではない
どうやって入れますか?
1. 手打ちする
入力UIがあるアプリなら都度手打ちして入れるのもありだと思います。少量データならこれで事足りることも。ただ、データ量が多くなるとやはり辛いです。
2. ハードコードする
サーバができていない、仕様もFixしていない、主機能の開発に集中したいなどの理由により、少なくとも開発初期段階はこれで進めることが多いと思います。
// ハードコード! let user = User(name: "Taro Yamada", // 名前 country: "Japan", // 国名 age: 30) // 年齢開発が進んだ段階では、もう少し何とかしたくなります。ソースコード中にゴミが書かれているのは嫌ですよね?
また、アプリが成長してくると、ビルド時間が長くなってくることがあります。サンプルデータをちょっと修正して再ビルドはストレスに感じるかもしれません。3. フェイクデータ生成ライブラリを使う
ランダムなデータで良ければ、Fakeryのようなフェイクデータ生成ライブラリを使うのもありです。ゴミデータがソースコード中に直に書かれているということは避けれますし、実行時にデータが生成されるため、再ビルドの必要もありません。
// Fakeryの使用例 let faker = Faker() let user = User(name: faker.name.name(), // 名前 country: faker.address.country(), // 国名 age: faker.number.randomInt(min: 20, max: 80)) // 年齢4. サーバからダウンロードする
Debugビルド時は開発サーバからデータをダウンロードする感じです。サーバを用意したり、ダウンローダーを書いたりとやることが多いですが、Release版に近い形で作れます。サーバ連携するアプリならきっと通る道です。
ただ、作業分担して開発しているとき、自分でサーバを触れるならば良いですが、そうでない場合はサーバ担当者にお願いする必要があり少々面倒です。
あと当たり前ですが、オンラインじゃないと使えません。
経験談ですが、客先でデモするとき、通信環境が必ずしも良好とは限りません。ロードに時間がかかっていると、けっこう気不味い空気になります…。デモ時はオフラインでもサクサク動かせるようにしておきたいですね。5. Mac・iOS間のファイル共有を利用する
Mac PCとiOS端末を直結してアプリにサンプルデータファイルを直接入れます。アプリはファイル読み込みしてデータをよしなに使えるように実装します。
最近、僕はこの方法をよく使うので紹介します。
実装方法
Info.plistに以下のプロパティを追加します。
Key Type Value Application supports iTunes file sharing Boolean YES このプロパティをYESをに設定したアプリは、Mac PCとiOS端末をケーブルで接続すると、FinderからアプリのDocumentsディレクトリが見れるようになります。(macOS 10.15より前はiTunes経由で見れましたが、10.15からはより直感的になりました)
ドラッグ&ドロップでファイルの転送ができるため、あとはアプリ側でDocumentsディレクトリ内の指定ファイルを読み込んでゴニョゴニョすればOKです。なんとでも書けてしまうわけですが、僕はサンプルデータ読込み用のクラスを作っています。ソースコードはこちら↓
https://github.com/HituziANDO/SampleData/blob/master/SampleDataApp/Util/SampleData.swiftこのクラスはDocumentsディレクトリ直下にsampledataディレクトリを配置し、その専用ディレクトリ内のファイルを読み込みます。ファイル形式はCodable+JSONDecoderの使い勝手が良いためJSONを扱います。
例えば、以下のようなuser.jsonファイルをsampledataディレクトリ直下に投入します。なお、シミュレータの場合は~/Library/Developer/CoreSimulator/Devices/{UUID}/data/Containers/Data/Application/{UUID}/Documents/sampledata
ディレクトリ配下にファイルを置いてください。(macOS 10.15.1, Xcode 11.2.1で確認)user.json{ "name": "山田 太郎", "country": "日本", "age": 30 }Userモデルは以下の通りです。
User.swiftstruct User: Codable { var name = "" var country = "" var age = 0 }読み込み方は以下の通りです。ファイル名と型を指定します。他にも重複して読み込まないようにするロックオプションなどがありますが、詳しくはソースコードを読んで頂ければと思います。
if let user = SampleData.default.import(dataOfFile: "user.json", ofType: User.self) { textView.text = """ User: name: \(user.name) country: \(user.country) age: \(user.age) """ }もし英語版のサンプルデータを入れたい時はJSONファイルを差し替えてアプリを起動すればOKです。ビルドにサンプルデータを含めているわけではないため、差し替え時に再ビルドの必要はありません。
user.json(英語版){ "name": "Brad Pitt", "country": "USA", "age": 55 }注意点
Documentsディレクトリがユーザに見える状態になるため、
- Releaseビルド時は
Application supports iTunes file sharing
をNOに設定する- ユーザに見られたくないファイルはLibraryディレクトリ配下に保存する
などセキュリティホールにならないようにしましょう。
サンプルアプリをGitHubにアップしておきましたので、ご参考にどうぞ。
https://github.com/HituziANDO/SampleDataまとめ
サンプルデータの入れ方について考察してみました。以下にメリット・デメリットをまとめておきます。
方法 メリット デメリット 手打ち 簡単 ・入力頻度、入力データが多いと辛い
・そもそも入力UIが必要ハードコード 簡単 ・ソースコードが汚くなる
・サンプルデータの差し替え時は再ビルドが必要フェイクデータ生成ライブラリ ・サンプルデータの生成が容易
・実行時にデータ生成するため、再ビルドが不要固定データがほしいときは使えない サーバからダウンロード ・サーバ連携するアプリならRelease版に近い形で作れる
・サーバ上でサンプルデータの差し替えが可能なため、アプリは再ビルドが不要・コストが高い
・オフラインでは使えないMac・iOS間のファイル共有 ・仕組みを実装した後はお手軽
・オフラインでファイルの差し替えが可能なため、再ビルドが不要Documentsディレクトリがユーザに見える状態になるので注意が必要
- 投稿日:2019-12-13T02:07:35+09:00
デザインのバージョン管理と開発連携をAbstractにまとめてエンジニアともっと仲良くなる
Classiアドベントカレンダー13日目は、デザイナーの@shio312がお送りします。
今年は弊社UXデザイン部も採用している、デザインのバージョン管理と開発連携をAbstractにまとめることになった経緯を、デザインツールの紹介を交えながらお話します。はじめに
エンジニアさん
「最新のデザインってどれ」
「えっ、実装結構進めてたのに急になんか違うUIになった(怒)」
「えっ、なんでこんなUIなの...わけがわからないよ...」
「しかも前回とはちがうツールでデザインカンプきた...なにこれ…つらい...」上記のような、残念な気持ちになるような事案に覚えはないでしょうか。ウッ。
私達はデザインシステムの構築やコンポーネント化、バージョン管理といった面で、プロトタイピングをinVisionかXDで、グラフィック作成をSketch、デザインレビューとバージョン管理をAbstractで行っていました。
問題だったのが開発連携ツールで一時期は乱立しており、inVisionのinspect機能、Abstractのinspect機能、さらにZeplinも試験的に導入し3つの開発連携ツールが使用されていて、デザイナー同士も連携されるエンジニアも「何を見れば最新のデザインが今どのような状況で、実装するなら何を見ればいいのか」のコミュニケーションにコストがかかっていました。UIツールを(XD)、SketchとAbstractに絞るまでの道のり
既存のデザインフローはこうだった
開発と連携まわりがもったいないかんじですね。運営も金銭コストもかかっています。
ということで開発との連携に使っている各ツールの特徴を整理し、ツールを絞っていきました。開発連携ツールの機能比較(2019年12月段階)
プロトタイプ バージョン管理 開発と連携 inVision
![]()
◎ × ○ Abstract
![]()
× ◎ ○ Zeplin
![]()
× × ◎ ツールそれぞれできることが見えてきました。
ここから絞るに当たってチームでツールごとに比較していきました。まずinVisionを除外
inVisionは少し前にinspect(開発連携するためのレイアウト指示ができる)機能が追加され、しばらく開発との連携ツールとしても使っていましたが、年間コストが高い上にプロトタイピングツールはXDで代替することができるので、ZeplinもしくはAbstractのinspect機能どちらかを使うとし、inVisionは除外となりました。〜 Abstract(inspect機能) VS Zeplin 〜
残る2ツールから1つに絞る際は慎重に比較しました。
それぞれのUIです。見た目はそこまで差がないですが、操作性に関して少し差がありました。
Abstract
(inspect機能)Zeplin pros ・Sketchデータそのままのレイヤー構成が反映され、デザイナーの意図が反映できる ・シンボルもしくはグループの最上層単位でオブジェクトとして認識されるのでレイヤー整理する必要がなくエンジニアに連携できる
・マージンの数字が一番正確cons ・レイヤーを整理しておかないと、どの数字を実装すればいいかがわかりづらい
・上記からレイヤー整理の工数が発生する・レイヤー名が表示されないので、レイアウト指示の際工夫が必要 比べてみると、inspect機能自体は老舗であるZeplinの方が精緻でスペックが少し高い結果に。
デザインフローを考えて、Abtractを選んだ
機能単体で比較するとZeplinを選ぶ結果になりましたが、ツール全体で見るとAbstractはデザインレビュー、バージョン管理、開発連携もでき、課題だった「何を見れば最新のデザインが今どのような状況で、実装するなら何を見ればいいのか」が1つのツールを把握していれば解決可能という優位性が決め手となり、Abstractを選択することになりました。
Abstractのinspect機能で発生する課題は「Sketchのレイヤーグループをまとめずに、オブジェクトごとに分ける」運用で対処することに。
ちなみにinVisionのinspectスペックはAbstractと同じ程度でした。新しいデザインフローはこうなった
Abstractでバージョン管理から開発連携まで一貫させる事で
- 管理コストを減らせる
- Abstractのプロジェクトにエンジニアを招待しデザインの変化を見せることで、実装中のエンジニアも最新のデザイン(設計)が把握できる
- エンジニアもデザインレビューに参加できる
- デザイナーもコミット履歴から仕様の変更の経緯が説明しやすくなり、エンジニアへレビューを投げるハードルが低くなる
Abstractのプロジェクトにディレクターなどチームメンバーも招待すれば、メンバー全員がデザイナーの進捗を把握できるようになる
みんながもっと仲良くなる?
※Figmaはどうなん?
バージョン管理まで可能なデザインフローを一貫して行えるFigmaですが、前述の通り弊社はデザインシステムの構築やコンポーネント化といった面でSketchをUIツールとして採用しています。プロジェクトの速さやフェーズによってはFigma一本の方が有効な場合もあります。さいごに_デザインにみんなを巻き込めばもっと仲良くなれる
まだ標準ツールとして設定して期は浅いのでまだまだ検証中ではありますが、徐々に「あのツールを確認すればわかる」という認識は広がっています。
弊社はAbstractを選びましたが、規模や進め方で違うツールを選んでもデザインにみんなが自主的に参加でき、把握できる環境を作る事ができればプロジェクトの風通しが良くなり、結果としてチームの雰囲気も良くなるはずです。
みなさんがよりよいプロダクト開発ができることを願っています。
- 投稿日:2019-12-13T02:04:40+09:00
僕のワンダフルライフ 〜ARのAI犬が友達〜
この記事は NewsPicks Advent Calendar 2019 の13日目の記事です。
はじめに
いらっしゃいませ、NewsPicksでエンジニアをしているkohei1218です。
突然ですが僕のワンダフルライフという映画をご存知でしょうか?
最近では続編の僕のワンダフル・ジャーニーも有名でした。
犬と飼い主の絆の物語なのですが、これがまあ泣ける映画で試写会では満足度100%という驚異的な数値を叩き出しました。
劇中では犬の健気な飼い主を愛する気持ちがセリフとなり、伝わってきます。犬好きな自分はよりこう思うわけです、犬飼いたいと。犬と話したいと。
しかし、私が住む築20年のアパートはそんなことを許してくれるはずがありません。だったらARで犬を表示して、さらに会話をできるようにしましょう。要件
ARで犬を表示する
→ARKitでいけそう音声入力で犬に話しかける
→SFSpeechRecognizerでいけそうAIで返事をする
→chat系のAPIがあればできそうその返事を読み上げる
→AVSpeechSynthesizerでいけそうあれ、これ1日でいける!?
ということで早速実装していきます。
ARで犬を表示する
まず3Dデータを準備します。
.obj
か.dae
のファイルをXcodeで.scn
に変えてあげる必要があります。Free 3D
こちらのサイトにフリーのいい感じの3Dファイルがたくさんあるので好きなものを選びましょう。今回はもちろん犬です。準備ができたら
New File
からSceneKit Catalog
を選択します。
この中にさきほどダウンロードしてきた
.obj
ファイルたちを入れてあげます。
そうしたら
obj
にカーソルを合わせてEditor
からConvert to SceneKit file format
を選択して.scn
ファイルに変換しましょう。
すると
.scn
ファイルが作られます。中身をみていくと
とんでもない向きになってますね。可愛いうちのわんちゃんを地面にめり込ませるわけにはいかないので向きを変更しましょう。3Dのオブジェクトを選択して
Node Inspector
のtransform
で向きを調整します。
うん、可愛い。名前付けたい
ニンテンドッグスとかにはまっちゃう私としてはたまりません。
早速このわんちゃんをARで表示していきます。ARを表示するための
ARSCNView
をviewにおき、シーンのセットとデバッグ情報を表示できるようにします。ViewController.swift@IBOutlet weak var sceneView: ARSCNView! override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene() sceneView.scene = scene sceneView.delegate = self sceneView.showsStatistics = true sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints }次にviewが表示されたらconfigurationを設定します。今回は水平な面にわんちゃんをおきたいので水平を検知する設定をいれます。
ViewController.swiftoverride func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = .horizontal sceneView.session.run(configuration) }これで水平な面が検知されたらdelegateで受け取ることができるのでそこでわんちゃんを置いていきましょう。
ViewController.swiftextension ViewController: ARSCNViewDelegate { func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor else { fatalError() } guard let scene = SCNScene(named: "dog.scn", inDirectory: "art.scnassets/dog") else { fatalError() } guard let catNode = scene.rootNode.childNode(withName: "Dog", recursively: true) else { fatalError() } let magnification = 0.005 catNode.scale = SCNVector3(magnification, magnification, magnification) catNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z) DispatchQueue.main.async(execute: { node.addChildNode(catNode) }) } }かんわぃぃぃぃ
ペット禁止の我が家についにわんちゃんが現れました。
可愛すぎてさわれないのが惜しいですがこのままやっていきましょう。
次はこのわんちゃんに話しかけられるようにします。音声入力で犬に話しかけたい
次に音声認識で声から文字列を取得していきます。
SFSpeechRecognizerを使用します。ViewController.swiftprivate let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))! private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? private var recognitionTask: SFSpeechRecognitionTask? private let audioEngine = AVAudioEngine() override func viewDidLoad() { super.viewDidLoad() speechRecognizer.delegate = self } @IBAction func tapeedRecording(_ sender: UILongPressGestureRecognizer) { switch sender.state{ case .began: try! start() recordingLabel.text = "認識中..." isRecording = true case .ended: recordImageView.isUserInteractionEnabled = false audioEngine.stop() recognitionRequest?.endAudio() audioEngine.inputNode.removeTap(onBus: 0) DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.recordImageView.isUserInteractionEnabled = true } default: break } } private func start() throws { guard !isRecording else { return } if let recognitionTask = recognitionTask { recognitionTask.cancel() self.recognitionTask = nil } let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.record, mode: .measurement, options: []) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) let recognitionRequest = SFSpeechAudioBufferRecognitionRequest() self.recognitionRequest = recognitionRequest recognitionRequest.shouldReportPartialResults = true recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] (result, error) in guard let `self` = self else { return } guard self.isRecording else { return } var isFinal = false if let result = result { isFinal = result.isFinal self.recordingLabel.text = result.bestTranscription.formattedString } if error != nil || isFinal { self.audioEngine.stop() self.audioEngine.inputNode.removeTap(onBus: 0) self.recognitionRequest = nil self.recognitionTask = nil } } let recordingFormat = audioEngine.inputNode.outputFormat(forBus: 0) audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in self.recognitionRequest?.append(buffer) } audioEngine.prepare() try? audioEngine.start() }これで音声入力で文字列を取得できるようになります。
次はここで取得したテキストをChatのAPIに投げていきましょう。
AIで返事をする
今回はユーザーローカルさんのSupportChatbotのAPIを使っていきましょう。
個人利用の場合は申請フォームから申し込むだけですぐにAPIのKeyが発行され、それと元に叩くだけで利用できます。
早速サンプルで投げてみましょう。https://chatbot-api.userlocal.jp/api/chat?message=こんにちは&key=APIKeyレスポンスがこちら
{ "status": "success", "result": "なーにー?" }めちゃくちゃ簡単。今回はこのmessageに音声認識で取得した文字列をセットして投げていきます。
ChatStruct.swiftstruct ChatStruct: Codable { var status: String var result: String }ChatApi.swiftclass ChatApi { static func getMessage(message: String, completion: @escaping (ChatStruct) -> Swift.Void) { let url = URL(string: "https://chatbot-api.userlocal.jp/api/chat?message=\(message)&key=your_api_key".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) let task = URLSession.shared.dataTask(with: url!) { data, response, error in guard let jsonData = data else { return } do { let chat = try JSONDecoder().decode(ChatStruct.self, from: jsonData) completion(chat) } catch { print(error.localizedDescription) } } task.resume() } }こんな感じでStructとModelを作成して音声認識が終わったらAPIを叩くようにして、結果を出力してみましょう。
ViewController.swift@IBAction func tapeedRecording(_ sender: UILongPressGestureRecognizer) { switch sender.state{ case .began: try! start() recordingLabel.text = "認識中..." isRecording = true case .ended: recordImageView.isUserInteractionEnabled = false audioEngine.stop() recognitionRequest?.endAudio() audioEngine.inputNode.removeTap(onBus: 0) loadingView.isHidden = false // 追加 ChatApi.getMessage(message: inputMessage) { [unowned self] (chat) in self.chat = chat DispatchQueue.main.async { self.loadingView.isHidden = true self.recordingLabel.text = nil // 出力してみる print("result:", chat.result) } self.isRecording = false } DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.recordImageView.isUserInteractionEnabled = true } default: break } }
こんにちは
と話しかけた結果がこちらresult: すごーっ絶妙に会話にはなっていないですがまあいいでしょう。最後にこの結果を読み上げてもらいましょう。
その返事を読み上げる
AVSpeechSynthesizerを使用して文字列を読み上げてもらいます。
ViewController.swiftprivate let speechSynthesizer = AVSpeechSynthesizer() private func speak(message: String) { defer { disableAVSession() } do { try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: .defaultToSpeaker) try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) } catch { print("audioSession properties weren't set because of an error.") } let utterance = AVSpeechUtterance(string: message) utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP") utterance.pitchMultiplier = 1 self.speechSynthesizer.speak(utterance) } private func disableAVSession() { do { try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } catch { print("audioSession properties weren't disable.") } }これで一通りの機能は完成したので早速試してみましょう。
声が機械的すぎてあれだけどちゃんと慰めてくれてますね。Qiitaに動画載せられなかったので音声は伝わらないのですが。。
結果
これで僕にも友達ができました。僕のワンダフルライフはここから始まります。
gitにもあげたので全体のソースはそこで確認してください。https://github.com/kohei1218/LoveMinus
明日は弊社のAndroidの神こと@kozmatsの記事です、お楽しみに!!
参考
ARKitで簡単ARやってみた
【ARKit】今日からはじめる AR プログラミング Part.1「現実空間をトラッキングする」
Swiftでリアルタイム音声認識するための最小コード
【Speech Framework】音声認識してテキストを入力する
[Swift] AVSpeechSynthesizerで読み上げ機能を使ってみる上記とてもわかりやすくて参考にしました、ありがとうございますm(_ _)m
- 投稿日:2019-12-13T00:57:07+09:00
Xcode11のコピペにつまずいた
はじめに
まずは下記をご覧いただきたい。
おわかりいただけただろうか?
Storyboard の ViewController を cmd + C, cmd + V でコピペしようとしたところ Xcode がクラッシュしたのである
現象
Xcode11 の Storyboard の ViewController をコピペしようとするとクラッシュする(Xcode11.0 11.2.1 で確認たまに View のコピペでも起きる...)
この現象はいつ起こるのか、なにが原因なのかは不明である。コピペできるときはできるし、できないときは何度やってもできない...
同一 Storyboard でのコピペ、別の Storyboard へのコピペ、別プロジェクトへのコピペいずれの場合も起こる
対応
stack overflow に同じ悩みを持つ人がいた
ここにいくつか対応策が書いてある。場合によってできるようになるみたいです。
cmd + D でコピペする
cmd + C, cmd + V ではなく cmd + D でコピペする。(はじめて知った...これでファイル複製できるらしい
)
私の場合はこれでできるようになりました。なぜか一度成功すると cmd + C, cmd + V でもコピペできるようになりました。シミュレータを停止する
シミュレータを起動している場合シミュレータを停止させます。(これでなおった人もいるみたい)
DerivedData のファイルを削除する
ライブラリ/Developer/Xcode/DerivedData 内のファイルを削除する。(念の為ゴミ箱からも削除)
XML として開きコピペする
こちらは別の Storyboard にコピペするときに有効な手段です。
- コピーしたい ViewController の Storyboard を Open As -> Souce Code で開く。
![]()
- 対象の ViewController の
<scene>
から</scene>
をコピーする。
![]()
- コピー先の Storyboard を Open As -> Souce Code で開く。
<scenes>
タグの中の適切な場所にコピーする。これで別の Storyboard に ViewController がコピペできます。同一 Storyboard 内の場合は ObjectID が重複してしまうので使えません
(下記のようなアラートが出ます)
さいごに
Xcode のバグみたいですが上記のいずれかの方法でコピペできれば幸いです
- 投稿日:2019-12-13T00:26:04+09:00
AzureSpatialAnchor2.0 と Unity で Hololens、iOS、Android の境界を超えた AR 体験を作る
こちらは AR Advent Calendar 2019 13日目の記事です。
はじめに
ARKit、ARCore を使ったモバイル端末での AR もさることながら、Hololens、MagicLeap だけでなく、Nreal などメガネ型デバイスも界隈を賑わせているかなと感じています。各デバイスでそれぞれの世界を見ながらも、共通の情報も見るという世界が、すぐ近い未来にきていると思っています。
そこで気が付いたら Azure Spatial Anchor のバージョンが 2.0 に上がっていたので、このサービスとUnityを使い、Hololens、iOS、Android の各端末から、同じ位置に同じオブジェクトを出して表示するという単純なAR体験をやってみようと思います。
Azure Spatial Anchor 2.0 を使ってみる
- クイック スタート:Azure Spatial Anchors を使用する iOS アプリを Swift または Objective-C で作成する
- チュートリアル:セッションやデバイス間での Azure Spatial Anchors の共有
を参考に、まずは Sample プログラムを動かしてみます。
環境
- Azure
- Spatial Anchors 2.0
- AppService
- Windows 10
- Unity 2019.2.15f1
- Visual Studio Community 2019 Version 16.4.0
- Mac Mojave 10.14.6
- Xcode 11.1
- 端末
- Hololens(2ではない) OS Build 10.0.17763.865
- iPhone X iOS 12.4
- Pixel XL Android 9.0
処理の流れ
こんな感じです。
1. ある端末で、周辺情報を取得
2. ↑ の端末で、オブジェクトを配置
3. ↑ の端末で、Anchor 情報を AppService 経由で Spatial Anchor に保存
4. 他の端末から、3 で保存した情報を取得、復元、そしてオブジェクトを配置実行結果
Hololens から見ている状態を写真にしたものです。①、②、③ それぞれが、Pixel、iPhone、Hololensで配置されているオブジェクトです。
所感
- Android、iOS、Hololens と各端末で共有できるというのは非常に有用なサービスですね。これからはこういうサービスを利用して、端末の壁を越えていくことになると思いました。
- おおむね同じような位置に配置されている気はしますが、ミリ単位と言う精度ではないかなと感じました。割と良い結果のものを撮ったので、センチ単位でずれが発生する感じはしました。こういうものは自ずと精度が上がってくると思うので、大きく気にするところではないと思いながら、今できることも気になるところです。
- デバイスの性能に依存するところが結構ありそうな感じがしました。もう少し新しい機種を利用して、動きのあるオブジェクトをおいかけるみたいなことを今後の課題にやれたら面白いかなと思いました。
- Azure Spatial Anchor が ver2.0 になったことで、GPS、Wifi を利用した位置情報も連携できるようです。こちらもまた今後の課題にしたいところです。
- そして Unity 全部開発できてすごい(語彙力)。ただ各ビルド方法が・・・。
メモ
参考 URL を見ながら、サンプルプログラムを動かせば大丈夫ですが、情報が古かったりするので、作業時のメモを残します。
クイック スタート:Azure Spatial Anchors を使用する iOS アプリを Swift または Objective-C で作成する
Unity 2019.1 以降および Visual Studio 2019 以降がインストールされている Windows コンピューターが必要です。 Visual Studio インストールには、ユニバーサル Windows プラットフォーム開発ワークロードと Windows 10 SDK (10.0.18362.0 以降) コンポーネントが含まれている必要があります。 Git for Windows もインストールする必要があります。
Windows 10 SDK (10.0.18362.0 以降) コンポーネント
とは、これのことです。
アプリで SpatialPerception 機能を有効にする必要があります。 この設定は、 [ビルド設定] > [プレーヤーの設定] > [発行の設定] > [機能] にあります。
アプリで Virtual Reality Supported を Windows Mixed Reality SDK と共に有効にする必要があります。 この設定は、 [ビルド設定] > [プレーヤーの設定] > XR Settings にあります。すぐにわかるかもしれませんが、
アプリ
と言うか、Unity です。Project ウィンドウで Assets\AzureSpatialAnchors.SDK\Resources に移動します。 [SpatialAnchorConfig] を選択します。 次に、 [Inspector] ウィンドウで、Spatial Anchors Account Key の値として Account Key を、Spatial Anchors Account Id の値として Account ID を入力します。
HoloLens デバイスの電源をオンにしてサインインし、USB ケーブルを使用してデバイスを PC に接続します。
[デバッグ] > [デバッグの開始] を選択して、アプリのデプロイとデバッグを開始します。休止状態で実行すると、こんな感じのエラーが出ます。かぶって電源入れて再度実行しましょう。
チュートリアル:セッションやデバイス間での Azure Spatial Anchors の共有
アカウント識別子とキーの構成